Skip to main content

nu_cli/
repl.rs

1use crate::prompt_update::{
2    POST_EXECUTION_MARKER_PREFIX, POST_EXECUTION_MARKER_SUFFIX, PRE_EXECUTION_MARKER,
3    RESET_APPLICATION_MODE, VSCODE_COMMANDLINE_MARKER_PREFIX, VSCODE_COMMANDLINE_MARKER_SUFFIX,
4    VSCODE_CWD_PROPERTY_MARKER_PREFIX, VSCODE_CWD_PROPERTY_MARKER_SUFFIX,
5    VSCODE_POST_EXECUTION_MARKER_PREFIX, VSCODE_POST_EXECUTION_MARKER_SUFFIX,
6    VSCODE_PRE_EXECUTION_MARKER,
7};
8use crate::{
9    NuHighlighter, NuValidator, NushellPrompt,
10    completions::NuCompleter,
11    nu_highlight::NoOpHighlighter,
12    prompt_update,
13    reedline_config::{KeybindingsMode, add_menus, create_keybindings},
14    util::eval_source,
15};
16use crossterm::cursor::SetCursorStyle;
17use log::{error, trace, warn};
18use miette::{ErrReport, IntoDiagnostic, Result};
19use nu_cmd_base::util::get_editor;
20use nu_color_config::StyleComputer;
21#[allow(deprecated)]
22use nu_engine::env_to_strings;
23use nu_engine::exit::cleanup_exit;
24use nu_parser::{lex, parse, trim_quotes_str};
25use nu_protocol::shell_error::io::IoError;
26use nu_protocol::{BannerKind, shell_error};
27use nu_protocol::{
28    Config, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value,
29    config::NuCursorShape,
30    engine::{EngineState, Stack, StateWorkingSet},
31    report_shell_error,
32};
33use nu_utils::{
34    filesystem::{PermissionResult, have_permission},
35    perf,
36};
37#[cfg(feature = "sqlite")]
38use reedline::SqliteBackedHistory;
39use reedline::{
40    CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
41    HistorySessionId, MouseClickMode, Osc133ClickEventsMarkers, Osc633Markers, Reedline,
42    SemanticPromptMarkers, Vi,
43};
44use std::sync::atomic::Ordering;
45use std::{
46    collections::HashMap,
47    env::temp_dir,
48    io::{self, IsTerminal, Write},
49    panic::{AssertUnwindSafe, catch_unwind},
50    path::{Path, PathBuf},
51    sync::Arc,
52    time::{Duration, Instant},
53};
54use sysinfo::System;
55
56fn semantic_markers_from_config(
57    config: &Config,
58    term_program_is_vscode: bool,
59) -> Option<Box<dyn SemanticPromptMarkers>> {
60    if config.shell_integration.osc633 && term_program_is_vscode {
61        Some(Osc633Markers::boxed())
62    } else if config.shell_integration.osc133 {
63        Some(Osc133ClickEventsMarkers::boxed())
64    } else {
65        None
66    }
67}
68
69/// The main REPL loop, including spinning up the prompt itself.
70pub fn evaluate_repl(
71    engine_state: &mut EngineState,
72    stack: Stack,
73    prerun_command: Option<Spanned<String>>,
74    load_std_lib: Option<Spanned<String>>,
75    entire_start_time: Instant,
76) -> Result<()> {
77    // throughout this code, we hold this stack uniquely.
78    // During the main REPL loop, we hand ownership of this value to an Arc,
79    // so that it may be read by various reedline plugins. During this, we
80    // can't modify the stack, but at the end of the loop we take back ownership
81    // from the Arc. This lets us avoid copying stack variables needlessly
82    let mut unique_stack = stack.clone();
83    let config = engine_state.get_config();
84    let use_color = config.use_ansi_coloring.get(engine_state);
85
86    let mut entry_num = 0;
87
88    // Let's grab the shell_integration configs
89    let shell_integration_osc2 = config.shell_integration.osc2;
90    let shell_integration_osc7 = config.shell_integration.osc7;
91    let shell_integration_osc9_9 = config.shell_integration.osc9_9;
92    let shell_integration_osc633 = config.shell_integration.osc633;
93
94    let nu_prompt = NushellPrompt::new();
95
96    // seed env vars
97    unique_stack.add_env_var(
98        "CMD_DURATION_MS".into(),
99        Value::string("0823", Span::unknown()),
100    );
101
102    unique_stack.set_last_exit_code(0, Span::unknown());
103
104    let mut line_editor = get_line_editor(engine_state, use_color)?;
105    let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
106
107    if let Some(s) = prerun_command {
108        eval_source(
109            engine_state,
110            &mut unique_stack,
111            s.item.as_bytes(),
112            &format!("repl_entry #{entry_num}"),
113            PipelineData::empty(),
114            false,
115        );
116        engine_state.merge_env(&mut unique_stack)?;
117    }
118
119    confirm_stdin_is_terminal()?;
120
121    let hostname = System::host_name();
122    if shell_integration_osc2 {
123        run_shell_integration_osc2(None, engine_state, &mut unique_stack, use_color);
124    }
125    if shell_integration_osc7 {
126        run_shell_integration_osc7(
127            hostname.as_deref(),
128            engine_state,
129            &mut unique_stack,
130            use_color,
131        );
132    }
133    if shell_integration_osc9_9 {
134        run_shell_integration_osc9_9(engine_state, &mut unique_stack, use_color);
135    }
136    if shell_integration_osc633 {
137        // escape a few things because this says so
138        // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
139        let cmd_text = line_editor.current_buffer_contents().to_string();
140
141        let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
142
143        run_shell_integration_osc633(
144            engine_state,
145            &mut unique_stack,
146            use_color,
147            replaced_cmd_text,
148        );
149    }
150
151    engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64);
152
153    // Regenerate the $nu constant to contain the startup time and any other potential updates
154    engine_state.generate_nu_constant();
155
156    if load_std_lib.is_none() {
157        match engine_state.get_config().show_banner {
158            BannerKind::None => {}
159            BannerKind::Short => {
160                eval_source(
161                    engine_state,
162                    &mut unique_stack,
163                    r#"banner --short"#.as_bytes(),
164                    "show short banner",
165                    PipelineData::empty(),
166                    false,
167                );
168            }
169            BannerKind::Full => {
170                eval_source(
171                    engine_state,
172                    &mut unique_stack,
173                    r#"banner"#.as_bytes(),
174                    "show_banner",
175                    PipelineData::empty(),
176                    false,
177                );
178            }
179        }
180    }
181
182    kitty_protocol_healthcheck(engine_state);
183
184    // Setup initial engine_state and stack state
185    let mut previous_engine_state = engine_state.clone();
186    let mut previous_stack_arc = Arc::new(unique_stack);
187    loop {
188        // clone these values so that they can be moved by AssertUnwindSafe
189        // If there is a panic within this iteration the last engine_state and stack
190        // will be used
191        let mut current_engine_state = previous_engine_state.clone();
192        // for the stack, we are going to hold to create a child stack instead,
193        // avoiding an expensive copy
194        let current_stack = Stack::with_parent(previous_stack_arc.clone());
195        let temp_file_cloned = temp_file.clone();
196        let mut nu_prompt_cloned = nu_prompt.clone();
197
198        let iteration_panic_state = catch_unwind(AssertUnwindSafe(|| {
199            let (continue_loop, current_stack, line_editor) = loop_iteration(LoopContext {
200                engine_state: &mut current_engine_state,
201                stack: current_stack,
202                line_editor,
203                nu_prompt: &mut nu_prompt_cloned,
204                temp_file: &temp_file_cloned,
205                use_color,
206                entry_num: &mut entry_num,
207                hostname: hostname.as_deref(),
208            });
209
210            // pass the most recent version of the line_editor back
211            (
212                continue_loop,
213                current_engine_state,
214                current_stack,
215                line_editor,
216            )
217        }));
218        match iteration_panic_state {
219            Ok((continue_loop, mut es, s, le)) => {
220                // We apply the changes from the updated stack back onto our previous stack
221                let mut merged_stack = Stack::with_changes_from_child(previous_stack_arc, s);
222
223                // Check if new variables were created (indicating potential variable shadowing)
224                let prev_total_vars = previous_engine_state.num_vars();
225                let curr_total_vars = es.num_vars();
226                let new_variables_created = curr_total_vars > prev_total_vars;
227
228                if new_variables_created {
229                    // New variables created, clean up stack to prevent memory leaks
230                    es.cleanup_stack_variables(&mut merged_stack);
231                }
232
233                previous_stack_arc = Arc::new(merged_stack);
234                // setup state for the next iteration of the repl loop
235                previous_engine_state = es;
236                line_editor = le;
237                if !continue_loop {
238                    break;
239                }
240            }
241            Err(_) => {
242                // line_editor is lost in the error case so reconstruct a new one
243                line_editor = get_line_editor(engine_state, use_color)?;
244            }
245        }
246    }
247
248    Ok(())
249}
250
251fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
252    let bytes = input
253        .chars()
254        .flat_map(|c| {
255            let mut buf = [0; 4]; // Buffer to hold UTF-8 bytes of the character
256            let c_bytes = c.encode_utf8(&mut buf); // Get UTF-8 bytes for the character
257
258            if c_bytes.len() == 1 {
259                let byte = c_bytes.as_bytes()[0];
260
261                match byte {
262                    // Escape bytes below 0x20
263                    b if b < 0x20 => format!("\\x{byte:02X}").into_bytes(),
264                    // Escape semicolon as \x3B
265                    b';' => "\\x3B".to_string().into_bytes(),
266                    // Escape backslash as \\
267                    b'\\' => "\\\\".to_string().into_bytes(),
268                    // Otherwise, return the character unchanged
269                    _ => vec![byte],
270                }
271            } else {
272                // pass through multi-byte characters unchanged
273                c_bytes.bytes().collect()
274            }
275        })
276        .collect();
277
278    String::from_utf8(bytes).map_err(|err| ShellError::CantConvert {
279        to_type: "string".to_string(),
280        from_type: "bytes".to_string(),
281        span: Span::unknown(),
282        help: Some(format!(
283            "Error {err}, Unable to convert {input} to escaped bytes"
284        )),
285    })
286}
287
288fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
289    let mut start_time = std::time::Instant::now();
290    let mut line_editor = Reedline::create();
291
292    // Now that reedline is created, get the history session id and store it in engine_state
293    store_history_id_in_engine(engine_state, &line_editor);
294    perf!("setup reedline", start_time, use_color);
295
296    if let Some(history) = engine_state.history_config() {
297        start_time = std::time::Instant::now();
298
299        line_editor = setup_history(engine_state, line_editor, history)?;
300
301        perf!("setup history", start_time, use_color);
302    }
303    Ok(line_editor)
304}
305
306struct LoopContext<'a> {
307    engine_state: &'a mut EngineState,
308    stack: Stack,
309    line_editor: Reedline,
310    nu_prompt: &'a mut NushellPrompt,
311    temp_file: &'a Path,
312    use_color: bool,
313    entry_num: &'a mut usize,
314    hostname: Option<&'a str>,
315}
316
317/// Perform one iteration of the REPL loop
318/// Result is bool: continue loop, current reedline
319#[inline]
320fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
321    use nu_cmd_base::hook;
322    use reedline::Signal;
323    let loop_start_time = std::time::Instant::now();
324
325    let LoopContext {
326        engine_state,
327        mut stack,
328        line_editor,
329        nu_prompt,
330        temp_file,
331        use_color,
332        entry_num,
333        hostname,
334    } = ctx;
335
336    let mut start_time = std::time::Instant::now();
337    // Before doing anything, merge the environment from the previous REPL iteration into the
338    // permanent state.
339    if let Err(err) = engine_state.merge_env(&mut stack) {
340        report_shell_error(None, engine_state, &err);
341    }
342    perf!("merge env", start_time, use_color);
343
344    start_time = std::time::Instant::now();
345    engine_state.reset_signals();
346    perf!("reset signals", start_time, use_color);
347
348    start_time = std::time::Instant::now();
349    // Check all the environment variables they ask for
350    // fire the "env_change" hook
351    if let Err(error) = hook::eval_env_change_hook(
352        &engine_state.get_config().hooks.env_change.clone(),
353        engine_state,
354        &mut stack,
355    ) {
356        report_shell_error(None, engine_state, &error)
357    }
358    perf!("env-change hook", start_time, use_color);
359
360    start_time = std::time::Instant::now();
361    // Next, right before we start our prompt and take input from the user, fire the "pre_prompt" hook
362    if let Err(err) = hook::eval_hooks(
363        engine_state,
364        &mut stack,
365        vec![],
366        &engine_state.get_config().hooks.pre_prompt.clone(),
367        "pre_prompt",
368    ) {
369        report_shell_error(None, engine_state, &err);
370    }
371    perf!("pre-prompt hook", start_time, use_color);
372
373    let engine_reference = Arc::new(engine_state.clone());
374    let config = stack.get_config(engine_state);
375
376    start_time = std::time::Instant::now();
377    // Find the configured cursor shapes for each mode
378    let cursor_config = CursorConfig {
379        vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
380        vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
381        emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
382    };
383    perf!("get config/cursor config", start_time, use_color);
384
385    start_time = std::time::Instant::now();
386    // at this line we have cloned the state for the completer and the transient prompt
387    // until we drop those, we cannot use the stack in the REPL loop itself
388    // See STACK-REFERENCE to see where we have taken a reference
389    let stack_arc = Arc::new(stack);
390    let term_program_is_vscode = engine_state
391        .get_env_var("TERM_PROGRAM")
392        .and_then(|v| v.as_str().ok())
393        == Some("vscode");
394    let mut line_editor = line_editor
395        .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
396        // try to enable bracketed paste
397        // It doesn't work on windows system: https://github.com/crossterm-rs/crossterm/issues/737
398        .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
399        .with_highlighter(Box::new(NuHighlighter {
400            engine_state: engine_reference.clone(),
401            // STACK-REFERENCE 1
402            stack: stack_arc.clone(),
403        }))
404        .with_validator(Box::new(NuValidator {
405            engine_state: engine_reference.clone(),
406        }))
407        .with_completer(Box::new(NuCompleter::new(
408            engine_reference.clone(),
409            // STACK-REFERENCE 2
410            stack_arc.clone(),
411        )))
412        .with_quick_completions(config.completions.quick)
413        .with_partial_completions(config.completions.partial)
414        .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
415        .with_cwd(Some(
416            engine_state
417                .cwd(None)
418                .map(|cwd| cwd.into_std_path_buf())
419                .unwrap_or_default()
420                .to_string_lossy()
421                .to_string(),
422        ))
423        .with_cursor_config(cursor_config)
424        .with_visual_selection_style(nu_ansi_term::Style {
425            is_reverse: true,
426            ..Default::default()
427        })
428        .with_semantic_markers(semantic_markers_from_config(
429            &config,
430            term_program_is_vscode,
431        ))
432        .with_mouse_click(if config.shell_integration.osc133 {
433            MouseClickMode::Enabled
434        } else {
435            MouseClickMode::Disabled
436        });
437
438    perf!("reedline builder", start_time, use_color);
439
440    let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
441
442    start_time = std::time::Instant::now();
443    line_editor = if config.use_ansi_coloring.get(engine_state) && config.show_hints {
444        line_editor.with_hinter(Box::new({
445            // As of Nov 2022, "hints" color_config closures only get `null` passed in.
446            let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
447            CwdAwareHinter::default().with_style(style)
448        }))
449    } else {
450        line_editor.disable_hints()
451    };
452
453    perf!("reedline coloring/style_computer", start_time, use_color);
454
455    start_time = std::time::Instant::now();
456    trace!("adding menus");
457    line_editor =
458        add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
459            report_shell_error(None, engine_state, &e);
460            Reedline::create()
461        });
462
463    perf!("reedline adding menus", start_time, use_color);
464
465    start_time = std::time::Instant::now();
466    let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
467
468    line_editor = if let Ok((cmd, args)) = buffer_editor {
469        let mut command = std::process::Command::new(cmd);
470        let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
471            warn!("Couldn't convert environment variable values to strings: {e}");
472            HashMap::default()
473        });
474        command.args(args).envs(envs);
475        line_editor.with_buffer_editor(command, temp_file.to_path_buf())
476    } else {
477        line_editor
478    };
479
480    perf!("reedline buffer_editor", start_time, use_color);
481
482    if let Some(history) = engine_state.history_config() {
483        start_time = std::time::Instant::now();
484        if history.sync_on_enter
485            && let Err(e) = line_editor.sync_history()
486        {
487            warn!("Failed to sync history: {e}");
488        }
489
490        perf!("sync_history", start_time, use_color);
491    }
492
493    start_time = std::time::Instant::now();
494    // Changing the line editor based on the found keybindings
495    line_editor = setup_keybindings(engine_state, line_editor);
496
497    perf!("keybindings", start_time, use_color);
498
499    start_time = std::time::Instant::now();
500    let config = &engine_state.get_config().clone();
501    prompt_update::update_prompt(
502        config,
503        engine_state,
504        &mut Stack::with_parent(stack_arc.clone()),
505        nu_prompt,
506    );
507    let transient_prompt = prompt_update::make_transient_prompt(
508        config,
509        engine_state,
510        &mut Stack::with_parent(stack_arc.clone()),
511        nu_prompt,
512    );
513
514    perf!("update_prompt", start_time, use_color);
515
516    *entry_num += 1;
517
518    start_time = std::time::Instant::now();
519    line_editor = line_editor.with_transient_prompt(transient_prompt);
520    let input = line_editor.read_line(nu_prompt);
521    // we got our inputs, we can now drop our stack references
522    // This lists all of the stack references that we have cleaned up
523    line_editor = line_editor
524        // CLEAR STACK-REFERENCE 1
525        .with_highlighter(Box::<NoOpHighlighter>::default())
526        // CLEAR STACK-REFERENCE 2
527        .with_completer(Box::<DefaultCompleter>::default())
528        // Ensure immediately accept is always cleared
529        .with_immediately_accept(false);
530
531    // Let's grab the shell_integration configs
532    let shell_integration_osc2 = config.shell_integration.osc2;
533    let shell_integration_osc7 = config.shell_integration.osc7;
534    let shell_integration_osc9_9 = config.shell_integration.osc9_9;
535    let shell_integration_osc133 = config.shell_integration.osc133;
536    let shell_integration_osc633 = config.shell_integration.osc633;
537    let shell_integration_reset_application_mode = config.shell_integration.reset_application_mode;
538
539    // TODO: we may clone the stack, this can lead to major performance issues
540    // so we should avoid it or making stack cheaper to clone.
541    let mut stack = Arc::unwrap_or_clone(stack_arc);
542
543    perf!("line_editor setup", start_time, use_color);
544
545    let line_editor_input_time = std::time::Instant::now();
546    match input {
547        Ok(Signal::Success(repl_cmd_line_text)) => {
548            let history_supports_meta = match engine_state.history_config().map(|h| h.file_format) {
549                #[cfg(feature = "sqlite")]
550                Some(HistoryFileFormat::Sqlite) => true,
551                _ => false,
552            };
553
554            if history_supports_meta {
555                prepare_history_metadata(
556                    &repl_cmd_line_text,
557                    hostname,
558                    engine_state,
559                    &mut line_editor,
560                );
561            }
562
563            // For pre_exec_hook
564            start_time = Instant::now();
565
566            // Right before we start running the code the user gave us, fire the `pre_execution`
567            // hook
568            {
569                // Set the REPL buffer to the current command for the "pre_execution" hook
570                let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
571                repl.buffer = repl_cmd_line_text.to_string();
572                drop(repl);
573
574                if let Err(err) = hook::eval_hooks(
575                    engine_state,
576                    &mut stack,
577                    vec![],
578                    &engine_state.get_config().hooks.pre_execution.clone(),
579                    "pre_execution",
580                ) {
581                    report_shell_error(None, engine_state, &err);
582                }
583            }
584
585            perf!("pre_execution_hook", start_time, use_color);
586
587            let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
588            repl.cursor_pos = line_editor.current_insertion_point();
589            repl.buffer = line_editor.current_buffer_contents().to_string();
590            drop(repl);
591
592            if shell_integration_osc633 {
593                if stack
594                    .get_env_var(engine_state, "TERM_PROGRAM")
595                    .and_then(|v| v.as_str().ok())
596                    == Some("vscode")
597                {
598                    start_time = Instant::now();
599
600                    run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
601
602                    perf!(
603                        "pre_execute_marker (633;C) ansi escape sequence",
604                        start_time,
605                        use_color
606                    );
607                } else if shell_integration_osc133 {
608                    start_time = Instant::now();
609
610                    run_ansi_sequence(PRE_EXECUTION_MARKER);
611
612                    perf!(
613                        "pre_execute_marker (133;C) ansi escape sequence",
614                        start_time,
615                        use_color
616                    );
617                }
618            } else if shell_integration_osc133 {
619                start_time = Instant::now();
620
621                run_ansi_sequence(PRE_EXECUTION_MARKER);
622
623                perf!(
624                    "pre_execute_marker (133;C) ansi escape sequence",
625                    start_time,
626                    use_color
627                );
628            }
629
630            // Actual command execution logic starts from here
631            let cmd_execution_start_time = Instant::now();
632
633            match parse_operation(repl_cmd_line_text.clone(), engine_state, &stack) {
634                Ok(operation) => match operation {
635                    ReplOperation::AutoCd { cwd, target, span } => {
636                        do_auto_cd(target, cwd, &mut stack, engine_state, span);
637
638                        run_finaliziation_ansi_sequence(
639                            &stack,
640                            engine_state,
641                            use_color,
642                            shell_integration_osc633,
643                            shell_integration_osc133,
644                        );
645                    }
646                    ReplOperation::RunCommand(cmd) => {
647                        line_editor = do_run_cmd(
648                            &cmd,
649                            &mut stack,
650                            engine_state,
651                            line_editor,
652                            shell_integration_osc2,
653                            *entry_num,
654                            use_color,
655                        );
656
657                        run_finaliziation_ansi_sequence(
658                            &stack,
659                            engine_state,
660                            use_color,
661                            shell_integration_osc633,
662                            shell_integration_osc133,
663                        );
664                    }
665                    // as the name implies, we do nothing in this case
666                    ReplOperation::DoNothing => {}
667                },
668                Err(ref e) => error!("Error parsing operation: {e}"),
669            }
670            let cmd_duration = cmd_execution_start_time.elapsed();
671
672            stack.add_env_var(
673                "CMD_DURATION_MS".into(),
674                Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
675            );
676
677            if history_supports_meta
678                && let Err(e) = fill_in_result_related_history_metadata(
679                    &repl_cmd_line_text,
680                    engine_state,
681                    cmd_duration,
682                    &mut stack,
683                    &mut line_editor,
684                )
685            {
686                warn!("Could not fill in result related history metadata: {e}");
687            }
688
689            if shell_integration_osc2 {
690                run_shell_integration_osc2(None, engine_state, &mut stack, use_color);
691            }
692            if shell_integration_osc7 {
693                run_shell_integration_osc7(hostname, engine_state, &mut stack, use_color);
694            }
695            if shell_integration_osc9_9 {
696                run_shell_integration_osc9_9(engine_state, &mut stack, use_color);
697            }
698            if shell_integration_osc633 {
699                run_shell_integration_osc633(
700                    engine_state,
701                    &mut stack,
702                    use_color,
703                    repl_cmd_line_text,
704                );
705            }
706            if shell_integration_reset_application_mode {
707                run_shell_integration_reset_application_mode();
708            }
709
710            line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
711        }
712        Ok(Signal::CtrlC) => {
713            // `Reedline` clears the line content. New prompt is shown
714            run_finaliziation_ansi_sequence(
715                &stack,
716                engine_state,
717                use_color,
718                shell_integration_osc633,
719                shell_integration_osc133,
720            );
721        }
722        Ok(Signal::CtrlD) => {
723            // When exiting clear to a new line
724
725            run_finaliziation_ansi_sequence(
726                &stack,
727                engine_state,
728                use_color,
729                shell_integration_osc633,
730                shell_integration_osc133,
731            );
732
733            println!();
734
735            cleanup_exit((), engine_state, 0);
736
737            // if cleanup_exit didn't exit, we should keep running
738            return (true, stack, line_editor);
739        }
740        Err(err) => {
741            let message = err.to_string();
742            if !message.contains("duration") {
743                eprintln!("Error: {err:?}");
744                // TODO: Identify possible error cases where a hard failure is preferable
745                // Ignoring and reporting could hide bigger problems
746                // e.g. https://github.com/nushell/nushell/issues/6452
747                // Alternatively only allow that expected failures let the REPL loop
748            }
749
750            run_finaliziation_ansi_sequence(
751                &stack,
752                engine_state,
753                use_color,
754                shell_integration_osc633,
755                shell_integration_osc133,
756            );
757        }
758    }
759    perf!(
760        "processing line editor input",
761        line_editor_input_time,
762        use_color
763    );
764
765    perf!(
766        "time between prompts in line editor loop",
767        loop_start_time,
768        use_color
769    );
770
771    (true, stack, line_editor)
772}
773
774///
775/// Put in history metadata not related to the result of running the command
776///
777fn prepare_history_metadata(
778    s: &str,
779    hostname: Option<&str>,
780    engine_state: &EngineState,
781    line_editor: &mut Reedline,
782) {
783    if !s.is_empty() && line_editor.has_last_command_context() {
784        let result = line_editor
785            .update_last_command_context(&|mut c| {
786                c.start_timestamp = Some(chrono::Utc::now());
787                c.hostname = hostname.map(str::to_string);
788                c.cwd = engine_state
789                    .cwd(None)
790                    .ok()
791                    .map(|path| path.to_string_lossy().to_string());
792                c
793            })
794            .into_diagnostic();
795        if let Err(e) = result {
796            warn!("Could not prepare history metadata: {e}");
797        }
798    }
799}
800
801///
802/// Fills in history item metadata based on the execution result (notably duration and exit code)
803///
804fn fill_in_result_related_history_metadata(
805    s: &str,
806    engine_state: &EngineState,
807    cmd_duration: Duration,
808    stack: &mut Stack,
809    line_editor: &mut Reedline,
810) -> Result<()> {
811    if !s.is_empty() && line_editor.has_last_command_context() {
812        line_editor
813            .update_last_command_context(&|mut c| {
814                c.duration = Some(cmd_duration);
815                c.exit_status = stack
816                    .get_env_var(engine_state, "LAST_EXIT_CODE")
817                    .and_then(|e| e.as_int().ok());
818                c
819            })
820            .into_diagnostic()?; // todo: don't stop repl if error here?
821    }
822    Ok(())
823}
824
825/// The kinds of operations you can do in a single loop iteration of the REPL
826enum ReplOperation {
827    /// "auto-cd": change directory by typing it in directly
828    AutoCd {
829        /// the current working directory
830        cwd: String,
831        /// the target
832        target: PathBuf,
833        /// span information for debugging
834        span: Span,
835    },
836    /// run a command
837    RunCommand(String),
838    /// do nothing (usually through an empty string)
839    DoNothing,
840}
841
842///
843/// Parses one "REPL line" of input, to try and derive intent.
844/// Notably, this is where we detect whether the user is attempting an
845/// "auto-cd" (writing a relative path directly instead of `cd path`)
846///
847/// Returns the ReplOperation we believe the user wants to do
848///
849fn parse_operation(
850    s: String,
851    engine_state: &EngineState,
852    stack: &Stack,
853) -> Result<ReplOperation, ErrReport> {
854    let tokens = lex(s.as_bytes(), 0, &[], &[], false);
855    // Check if this is a single call to a directory, if so auto-cd
856    let cwd = engine_state
857        .cwd(Some(stack))
858        .map(|p| p.to_string_lossy().to_string())
859        .unwrap_or_default();
860    let mut orig = s.clone();
861    if orig.starts_with('`') {
862        orig = trim_quotes_str(&orig).to_string()
863    }
864
865    let path = nu_path::expand_path_with(&orig, &cwd, true);
866    if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 {
867        Ok(ReplOperation::AutoCd {
868            cwd,
869            target: path,
870            span: tokens.0[0].span,
871        })
872    } else if !s.trim().is_empty() {
873        Ok(ReplOperation::RunCommand(s))
874    } else {
875        Ok(ReplOperation::DoNothing)
876    }
877}
878
879///
880/// Execute an "auto-cd" operation, changing the current working directory.
881///
882fn do_auto_cd(
883    path: PathBuf,
884    cwd: String,
885    stack: &mut Stack,
886    engine_state: &mut EngineState,
887    span: Span,
888) {
889    let path = {
890        if !path.exists() {
891            report_shell_error(
892                Some(stack),
893                engine_state,
894                &ShellError::Io(IoError::new_with_additional_context(
895                    shell_error::io::ErrorKind::DirectoryNotFound,
896                    span,
897                    PathBuf::from(&path),
898                    "Cannot change directory",
899                )),
900            );
901        }
902        path.to_string_lossy().to_string()
903    };
904
905    if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
906        report_shell_error(
907            Some(stack),
908            engine_state,
909            &ShellError::Io(IoError::new_with_additional_context(
910                shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
911                span,
912                PathBuf::from(path),
913                "Cannot change directory",
914            )),
915        );
916        return;
917    }
918
919    stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown()));
920
921    //FIXME: this only changes the current scope, but instead this environment variable
922    //should probably be a block that loads the information from the state in the overlay
923    if let Err(err) = stack.set_cwd(&path) {
924        report_shell_error(Some(stack), engine_state, &err);
925        return;
926    };
927    let cwd = Value::string(cwd, span);
928
929    let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
930    let mut shells = if let Some(v) = shells {
931        v.clone().into_list().unwrap_or_else(|_| vec![cwd])
932    } else {
933        vec![cwd]
934    };
935
936    let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
937    let current_shell = if let Some(v) = current_shell {
938        v.as_int().unwrap_or_default() as usize
939    } else {
940        0
941    };
942
943    let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
944    let last_shell = if let Some(v) = last_shell {
945        v.as_int().unwrap_or_default() as usize
946    } else {
947        0
948    };
949
950    shells[current_shell] = Value::string(path, span);
951
952    stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
953    stack.add_env_var(
954        "NUSHELL_LAST_SHELL".into(),
955        Value::int(last_shell as i64, span),
956    );
957    stack.set_last_exit_code(0, Span::unknown());
958}
959
960///
961/// Run a command as received from reedline. This is where we are actually
962/// running a thing!
963///
964fn do_run_cmd(
965    s: &str,
966    stack: &mut Stack,
967    engine_state: &mut EngineState,
968    // we pass in the line editor so it can be dropped in the case of a process exit
969    // (in the normal case we don't want to drop it so return it as-is otherwise)
970    line_editor: Reedline,
971    shell_integration_osc2: bool,
972    entry_num: usize,
973    use_color: bool,
974) -> Reedline {
975    trace!("eval source: {s}");
976
977    let mut cmds = s.split_whitespace();
978
979    let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
980
981    if let Some("exit") = cmds.next() {
982        let mut working_set = StateWorkingSet::new(engine_state);
983        let _ = parse(&mut working_set, None, s.as_bytes(), false);
984
985        if working_set.parse_errors.is_empty() {
986            match cmds.next() {
987                Some(s) => {
988                    if let Ok(n) = s.parse::<i32>() {
989                        return cleanup_exit(line_editor, engine_state, n);
990                    }
991                }
992                None => {
993                    return cleanup_exit(line_editor, engine_state, 0);
994                }
995            }
996        }
997    }
998
999    if shell_integration_osc2 {
1000        run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
1001    }
1002
1003    eval_source(
1004        engine_state,
1005        stack,
1006        s.as_bytes(),
1007        &format!("repl_entry #{entry_num}"),
1008        PipelineData::empty(),
1009        false,
1010    );
1011
1012    // if there was a warning before, and we got to this point, it means
1013    // the possible call to cleanup_exit did not occur.
1014    if had_warning_before && engine_state.is_interactive {
1015        engine_state
1016            .exit_warning_given
1017            .store(false, Ordering::SeqCst);
1018    }
1019
1020    line_editor
1021}
1022
1023///
1024/// Output some things and set environment variables so shells with the right integration
1025/// can have more information about what is going on (both on startup and after we have
1026/// run a command)
1027///
1028fn run_shell_integration_osc2(
1029    command_name: Option<&str>,
1030    engine_state: &EngineState,
1031    stack: &mut Stack,
1032    use_color: bool,
1033) {
1034    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1035        let start_time = Instant::now();
1036
1037        // Try to abbreviate string for windows title
1038        let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1039            let home_dir_str = p.as_path().display().to_string();
1040            if path.starts_with(&home_dir_str) {
1041                path.replacen(&home_dir_str, "~", 1)
1042            } else {
1043                path
1044            }
1045        } else {
1046            path
1047        };
1048
1049        let title = match command_name {
1050            Some(binary_name) => {
1051                let split_binary_name = binary_name.split_whitespace().next();
1052                if let Some(binary_name) = split_binary_name {
1053                    format!("{maybe_abbrev_path}> {binary_name}")
1054                } else {
1055                    maybe_abbrev_path.to_string()
1056                }
1057            }
1058            None => maybe_abbrev_path.to_string(),
1059        };
1060
1061        // Set window title too
1062        // https://tldp.org/HOWTO/Xterm-Title-3.html
1063        // ESC]0;stringBEL -- Set icon name and window title to string
1064        // ESC]1;stringBEL -- Set icon name to string
1065        // ESC]2;stringBEL -- Set window title to string
1066        run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1067
1068        perf!("set title with command osc2", start_time, use_color);
1069    }
1070}
1071
1072fn run_shell_integration_osc7(
1073    hostname: Option<&str>,
1074    engine_state: &EngineState,
1075    stack: &mut Stack,
1076    use_color: bool,
1077) {
1078    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1079        let start_time = Instant::now();
1080
1081        let path = if cfg!(windows) {
1082            path.replace('\\', "/")
1083        } else {
1084            path
1085        };
1086
1087        // Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
1088        run_ansi_sequence(&format!(
1089            "\x1b]7;file://{}{}{}\x1b\\",
1090            percent_encoding::utf8_percent_encode(
1091                hostname.unwrap_or("localhost"),
1092                percent_encoding::CONTROLS
1093            ),
1094            if path.starts_with('/') { "" } else { "/" },
1095            percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1096        ));
1097
1098        perf!(
1099            "communicate path to terminal with osc7",
1100            start_time,
1101            use_color
1102        );
1103    }
1104}
1105
1106fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1107    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1108        let start_time = Instant::now();
1109
1110        // Otherwise, communicate the path as OSC 9;9 from ConEmu (often used for spawning new tabs in the same dir)
1111        // This is helpful in Windows Terminal with Duplicate Tab
1112        run_ansi_sequence(&format!("\x1b]9;9;{}\x1b\\", path));
1113
1114        perf!(
1115            "communicate path to terminal with osc9;9",
1116            start_time,
1117            use_color
1118        );
1119    }
1120}
1121
1122fn run_shell_integration_osc633(
1123    engine_state: &EngineState,
1124    stack: &mut Stack,
1125    use_color: bool,
1126    repl_cmd_line_text: String,
1127) {
1128    if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1129        // Supported escape sequences of Microsoft's Visual Studio Code (vscode)
1130        // https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
1131        if stack
1132            .get_env_var(engine_state, "TERM_PROGRAM")
1133            .and_then(|v| v.as_str().ok())
1134            == Some("vscode")
1135        {
1136            let start_time = Instant::now();
1137
1138            // If we're in vscode, run their specific ansi escape sequence.
1139            // This is helpful for ctrl+g to change directories in the terminal.
1140            run_ansi_sequence(&format!(
1141                "{VSCODE_CWD_PROPERTY_MARKER_PREFIX}{path}{VSCODE_CWD_PROPERTY_MARKER_SUFFIX}"
1142            ));
1143
1144            perf!(
1145                "communicate path to terminal with osc633;P",
1146                start_time,
1147                use_color
1148            );
1149
1150            // escape a few things because this says so
1151            // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
1152            let replaced_cmd_text =
1153                escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1154
1155            //OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
1156            run_ansi_sequence(&format!(
1157                "{VSCODE_COMMANDLINE_MARKER_PREFIX}{replaced_cmd_text}{VSCODE_COMMANDLINE_MARKER_SUFFIX}"
1158            ));
1159        }
1160    }
1161}
1162
1163fn run_shell_integration_reset_application_mode() {
1164    run_ansi_sequence(RESET_APPLICATION_MODE);
1165}
1166
1167///
1168/// Clear the screen and output anything remaining in the EngineState buffer.
1169///
1170fn flush_engine_state_repl_buffer(
1171    engine_state: &mut EngineState,
1172    mut line_editor: Reedline,
1173) -> Reedline {
1174    let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1175    line_editor.run_edit_commands(&[
1176        EditCommand::Clear,
1177        EditCommand::InsertString(repl.buffer.to_string()),
1178        EditCommand::MoveToPosition {
1179            position: repl.cursor_pos,
1180            select: false,
1181        },
1182    ]);
1183    if repl.accept {
1184        line_editor = line_editor.with_immediately_accept(true)
1185    }
1186    repl.accept = false;
1187    repl.buffer = "".to_string();
1188    repl.cursor_pos = 0;
1189    line_editor
1190}
1191
1192///
1193/// Setup history management for Reedline
1194///
1195fn setup_history(
1196    engine_state: &mut EngineState,
1197    line_editor: Reedline,
1198    history: HistoryConfig,
1199) -> Result<Reedline> {
1200    // Setup history_isolation aka "history per session"
1201    let history_session_id = if history.isolation {
1202        Reedline::create_history_session_id()
1203    } else {
1204        None
1205    };
1206
1207    if let Some(path) = history.file_path() {
1208        return update_line_editor_history(
1209            engine_state,
1210            path,
1211            history,
1212            line_editor,
1213            history_session_id,
1214        );
1215    };
1216    Ok(line_editor)
1217}
1218
1219///
1220/// Setup Reedline keybindingds based on the provided config
1221///
1222fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1223    match create_keybindings(engine_state.get_config()) {
1224        Ok(keybindings) => match keybindings {
1225            KeybindingsMode::Emacs(keybindings) => {
1226                let edit_mode = Box::new(Emacs::new(keybindings));
1227                line_editor.with_edit_mode(edit_mode)
1228            }
1229            KeybindingsMode::Vi {
1230                insert_keybindings,
1231                normal_keybindings,
1232            } => {
1233                let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1234                line_editor.with_edit_mode(edit_mode)
1235            }
1236        },
1237        Err(e) => {
1238            report_shell_error(None, engine_state, &e);
1239            line_editor
1240        }
1241    }
1242}
1243
1244///
1245/// Make sure that the terminal supports the kitty protocol if the config is asking for it
1246///
1247fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1248    if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1249        warn!("Terminal doesn't support use_kitty_protocol config");
1250    }
1251}
1252
1253fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1254    let session_id = line_editor
1255        .get_history_session_id()
1256        .map(i64::from)
1257        .unwrap_or(0);
1258
1259    engine_state.history_session_id = session_id;
1260}
1261
1262fn update_line_editor_history(
1263    engine_state: &mut EngineState,
1264    history_path: PathBuf,
1265    history: HistoryConfig,
1266    line_editor: Reedline,
1267    history_session_id: Option<HistorySessionId>,
1268) -> Result<Reedline, ErrReport> {
1269    let history: Box<dyn reedline::History> = match history.file_format {
1270        HistoryFileFormat::Plaintext => Box::new(
1271            FileBackedHistory::with_file(history.max_size as usize, history_path)
1272                .into_diagnostic()?,
1273        ),
1274        // this path should not happen as the config setting is captured by `nu-protocol` already
1275        #[cfg(not(feature = "sqlite"))]
1276        HistoryFileFormat::Sqlite => {
1277            return Err(miette::miette!(
1278                help = "compile Nushell with the `sqlite` feature to use this",
1279                "Unsupported history file format",
1280            ));
1281        }
1282        #[cfg(feature = "sqlite")]
1283        HistoryFileFormat::Sqlite => Box::new(
1284            SqliteBackedHistory::with_file(
1285                history_path.to_path_buf(),
1286                history_session_id,
1287                Some(chrono::Utc::now()),
1288            )
1289            .into_diagnostic()?,
1290        ),
1291    };
1292    let line_editor = line_editor
1293        .with_history_session_id(history_session_id)
1294        .with_history_exclusion_prefix(Some(" ".into()))
1295        .with_history(history);
1296
1297    store_history_id_in_engine(engine_state, &line_editor);
1298
1299    Ok(line_editor)
1300}
1301
1302fn confirm_stdin_is_terminal() -> Result<()> {
1303    // Guard against invocation without a connected terminal.
1304    // reedline / crossterm event polling will fail without a connected tty
1305    if !std::io::stdin().is_terminal() {
1306        return Err(std::io::Error::new(
1307            std::io::ErrorKind::NotFound,
1308            "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1309        ))
1310        .into_diagnostic();
1311    }
1312    Ok(())
1313}
1314fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1315    match shape {
1316        NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1317        NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1318        NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1319        NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1320        NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1321        NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1322        NuCursorShape::Inherit => None,
1323    }
1324}
1325
1326fn get_command_finished_marker(
1327    stack: &Stack,
1328    engine_state: &EngineState,
1329    shell_integration_osc633: bool,
1330    shell_integration_osc133: bool,
1331) -> String {
1332    let exit_code = stack
1333        .get_env_var(engine_state, "LAST_EXIT_CODE")
1334        .and_then(|e| e.as_int().ok());
1335
1336    if shell_integration_osc633 {
1337        if stack
1338            .get_env_var(engine_state, "TERM_PROGRAM")
1339            .and_then(|v| v.as_str().ok())
1340            == Some("vscode")
1341        {
1342            // We're in vscode and we have osc633 enabled
1343            format!(
1344                "{}{}{}",
1345                VSCODE_POST_EXECUTION_MARKER_PREFIX,
1346                exit_code.unwrap_or(0),
1347                VSCODE_POST_EXECUTION_MARKER_SUFFIX
1348            )
1349        } else if shell_integration_osc133 {
1350            // If we're in VSCode but we don't find the env var, just return the regular markers
1351            format!(
1352                "{}{}{}",
1353                POST_EXECUTION_MARKER_PREFIX,
1354                exit_code.unwrap_or(0),
1355                POST_EXECUTION_MARKER_SUFFIX
1356            )
1357        } else {
1358            // We're not in vscode, so we don't need to do anything special
1359            "\x1b[0m".to_string()
1360        }
1361    } else if shell_integration_osc133 {
1362        format!(
1363            "{}{}{}",
1364            POST_EXECUTION_MARKER_PREFIX,
1365            exit_code.unwrap_or(0),
1366            POST_EXECUTION_MARKER_SUFFIX
1367        )
1368    } else {
1369        "\x1b[0m".to_string()
1370    }
1371}
1372
1373fn run_ansi_sequence(seq: &str) {
1374    if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1375        warn!("Error writing ansi sequence {e}");
1376    } else if let Err(e) = io::stdout().flush() {
1377        warn!("Error flushing stdio {e}");
1378    }
1379}
1380
1381fn run_finaliziation_ansi_sequence(
1382    stack: &Stack,
1383    engine_state: &EngineState,
1384    use_color: bool,
1385    shell_integration_osc633: bool,
1386    shell_integration_osc133: bool,
1387) {
1388    if shell_integration_osc633 {
1389        // Only run osc633 if we are in vscode
1390        if stack
1391            .get_env_var(engine_state, "TERM_PROGRAM")
1392            .and_then(|v| v.as_str().ok())
1393            == Some("vscode")
1394        {
1395            let start_time = Instant::now();
1396
1397            run_ansi_sequence(&get_command_finished_marker(
1398                stack,
1399                engine_state,
1400                shell_integration_osc633,
1401                shell_integration_osc133,
1402            ));
1403
1404            perf!(
1405                "post_execute_marker (633;D) ansi escape sequences",
1406                start_time,
1407                use_color
1408            );
1409        } else if shell_integration_osc133 {
1410            let start_time = Instant::now();
1411
1412            run_ansi_sequence(&get_command_finished_marker(
1413                stack,
1414                engine_state,
1415                shell_integration_osc633,
1416                shell_integration_osc133,
1417            ));
1418
1419            perf!(
1420                "post_execute_marker (133;D) ansi escape sequences",
1421                start_time,
1422                use_color
1423            );
1424        }
1425    } else if shell_integration_osc133 {
1426        let start_time = Instant::now();
1427
1428        run_ansi_sequence(&get_command_finished_marker(
1429            stack,
1430            engine_state,
1431            shell_integration_osc633,
1432            shell_integration_osc133,
1433        ));
1434
1435        perf!(
1436            "post_execute_marker (133;D) ansi escape sequences",
1437            start_time,
1438            use_color
1439        );
1440    }
1441}
1442
1443// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
1444#[cfg(windows)]
1445static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1446    fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1447});
1448
1449// A best-effort "does this string look kinda like a path?" function to determine whether to auto-cd
1450fn looks_like_path(orig: &str) -> bool {
1451    #[cfg(windows)]
1452    {
1453        if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1454            return true;
1455        }
1456    }
1457
1458    orig.starts_with('.')
1459        || orig.starts_with('~')
1460        || orig.starts_with('/')
1461        || orig.starts_with('\\')
1462        || orig.ends_with(std::path::MAIN_SEPARATOR)
1463}
1464
1465#[cfg(test)]
1466mod semantic_marker_tests {
1467    use super::semantic_markers_from_config;
1468    use nu_protocol::Config;
1469    use reedline::PromptKind;
1470
1471    #[test]
1472    fn semantic_markers_use_osc633_in_vscode() {
1473        let mut config = Config::default();
1474        config.shell_integration.osc633 = true;
1475        config.shell_integration.osc133 = true;
1476
1477        let markers =
1478            semantic_markers_from_config(&config, true).expect("expected semantic markers");
1479
1480        assert_eq!(
1481            markers.prompt_start(PromptKind::Primary).as_ref(),
1482            "\x1b]633;A;k=i\x1b\\"
1483        );
1484    }
1485
1486    #[test]
1487    fn semantic_markers_use_osc133_click_events() {
1488        let mut config = Config::default();
1489        config.shell_integration.osc133 = true;
1490
1491        let markers =
1492            semantic_markers_from_config(&config, false).expect("expected semantic markers");
1493
1494        assert_eq!(
1495            markers.prompt_start(PromptKind::Primary).as_ref(),
1496            "\x1b]133;A;k=i;click_events=1\x1b\\"
1497        );
1498    }
1499
1500    #[test]
1501    fn semantic_markers_none_when_disabled() {
1502        let mut config = Config::default();
1503        config.shell_integration.osc133 = false;
1504        config.shell_integration.osc633 = false;
1505        assert!(semantic_markers_from_config(&config, false).is_none());
1506    }
1507}
1508
1509#[cfg(windows)]
1510#[test]
1511fn looks_like_path_windows_drive_path_works() {
1512    assert!(looks_like_path("C:"));
1513    assert!(looks_like_path("D:\\"));
1514    assert!(looks_like_path("E:/"));
1515    assert!(looks_like_path("F:\\some_dir"));
1516    assert!(looks_like_path("G:/some_dir"));
1517}
1518
1519#[cfg(windows)]
1520#[test]
1521fn trailing_slash_looks_like_path() {
1522    assert!(looks_like_path("foo\\"))
1523}
1524
1525#[cfg(not(windows))]
1526#[test]
1527fn trailing_slash_looks_like_path() {
1528    assert!(looks_like_path("foo/"))
1529}
1530
1531#[test]
1532fn are_session_ids_in_sync() {
1533    let engine_state = &mut EngineState::new();
1534    let history = engine_state.history_config().unwrap();
1535    let history_path = history.file_path().unwrap();
1536    let line_editor = reedline::Reedline::create();
1537    let history_session_id = reedline::Reedline::create_history_session_id();
1538    let line_editor = update_line_editor_history(
1539        engine_state,
1540        history_path,
1541        history,
1542        line_editor,
1543        history_session_id,
1544    );
1545    assert_eq!(
1546        i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1547        engine_state.history_session_id
1548    );
1549}
1550
1551#[cfg(test)]
1552mod test_auto_cd {
1553    use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1554    use nu_path::AbsolutePath;
1555    use nu_protocol::engine::{EngineState, Stack};
1556    use tempfile::tempdir;
1557
1558    /// Create a symlink. Works on both Unix and Windows.
1559    #[cfg(any(unix, windows))]
1560    fn symlink(
1561        original: impl AsRef<AbsolutePath>,
1562        link: impl AsRef<AbsolutePath>,
1563    ) -> std::io::Result<()> {
1564        let original = original.as_ref();
1565        let link = link.as_ref();
1566
1567        #[cfg(unix)]
1568        {
1569            std::os::unix::fs::symlink(original, link)
1570        }
1571        #[cfg(windows)]
1572        {
1573            if original.is_dir() {
1574                std::os::windows::fs::symlink_dir(original, link)
1575            } else {
1576                std::os::windows::fs::symlink_file(original, link)
1577            }
1578        }
1579    }
1580
1581    /// Run one test case on the auto-cd feature. PWD is initially set to
1582    /// `before`, and after `input` is parsed and evaluated, PWD should be
1583    /// changed to `after`.
1584    #[track_caller]
1585    fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1586        // Setup EngineState and Stack.
1587        let mut engine_state = EngineState::new();
1588        let mut stack = Stack::new();
1589        stack.set_cwd(before.as_ref()).unwrap();
1590
1591        // Parse the input. It must be an auto-cd operation.
1592        let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1593        let ReplOperation::AutoCd { cwd, target, span } = op else {
1594            panic!("'{input}' was not parsed into an auto-cd operation")
1595        };
1596
1597        // Perform the auto-cd operation.
1598        do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1599        let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1600
1601        // Check that `updated_cwd` and `after` point to the same place. They
1602        // don't have to be byte-wise equal (on Windows, the 8.3 filename
1603        // conversion messes things up),
1604        let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1605        let after = std::fs::canonicalize(after.as_ref()).unwrap();
1606        assert_eq!(updated_cwd, after);
1607    }
1608
1609    #[test]
1610    fn auto_cd_root() {
1611        let tempdir = tempdir().unwrap();
1612        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1613
1614        let input = if cfg!(windows) { r"C:\" } else { "/" };
1615        let root = AbsolutePath::try_new(input).unwrap();
1616        check(tempdir, input, root);
1617    }
1618
1619    #[test]
1620    fn auto_cd_tilde() {
1621        let tempdir = tempdir().unwrap();
1622        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1623
1624        let home = nu_path::home_dir().unwrap();
1625        check(tempdir, "~", home);
1626    }
1627
1628    #[test]
1629    fn auto_cd_dot() {
1630        let tempdir = tempdir().unwrap();
1631        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1632
1633        check(tempdir, ".", tempdir);
1634    }
1635
1636    #[test]
1637    fn auto_cd_double_dot() {
1638        let tempdir = tempdir().unwrap();
1639        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1640
1641        let dir = tempdir.join("foo");
1642        std::fs::create_dir_all(&dir).unwrap();
1643        check(dir, "..", tempdir);
1644    }
1645
1646    #[test]
1647    fn auto_cd_triple_dot() {
1648        let tempdir = tempdir().unwrap();
1649        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1650
1651        let dir = tempdir.join("foo").join("bar");
1652        std::fs::create_dir_all(&dir).unwrap();
1653        check(dir, "...", tempdir);
1654    }
1655
1656    #[test]
1657    fn auto_cd_relative() {
1658        let tempdir = tempdir().unwrap();
1659        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1660
1661        let foo = tempdir.join("foo");
1662        let bar = tempdir.join("bar");
1663        std::fs::create_dir_all(&foo).unwrap();
1664        std::fs::create_dir_all(&bar).unwrap();
1665        let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1666        check(foo, input, bar);
1667    }
1668
1669    #[test]
1670    fn auto_cd_trailing_slash() {
1671        let tempdir = tempdir().unwrap();
1672        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1673
1674        let dir = tempdir.join("foo");
1675        std::fs::create_dir_all(&dir).unwrap();
1676        let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1677        check(tempdir, input, dir);
1678    }
1679
1680    #[test]
1681    fn auto_cd_symlink() {
1682        let tempdir = tempdir().unwrap();
1683        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1684
1685        let dir = tempdir.join("foo");
1686        std::fs::create_dir_all(&dir).unwrap();
1687        let link = tempdir.join("link");
1688        symlink(&dir, &link).unwrap();
1689        let input = if cfg!(windows) { r".\link" } else { "./link" };
1690        check(tempdir, input, link);
1691
1692        let dir = tempdir.join("foo").join("bar");
1693        std::fs::create_dir_all(&dir).unwrap();
1694        let link = tempdir.join("link2");
1695        symlink(&dir, &link).unwrap();
1696        let input = "..";
1697        check(link, input, tempdir);
1698    }
1699
1700    #[test]
1701    #[should_panic(expected = "was not parsed into an auto-cd operation")]
1702    fn auto_cd_nonexistent_directory() {
1703        let tempdir = tempdir().unwrap();
1704        let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1705
1706        let dir = tempdir.join("foo");
1707        let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1708        check(tempdir, input, dir);
1709    }
1710
1711    #[test]
1712    fn escape_vscode_semicolon_test() {
1713        let input = r#"now;is"#;
1714        let expected = r#"now\x3Bis"#;
1715        let actual = escape_special_vscode_bytes(input).unwrap();
1716        assert_eq!(expected, actual);
1717    }
1718
1719    #[test]
1720    fn escape_vscode_backslash_test() {
1721        let input = r#"now\is"#;
1722        let expected = r#"now\\is"#;
1723        let actual = escape_special_vscode_bytes(input).unwrap();
1724        assert_eq!(expected, actual);
1725    }
1726
1727    #[test]
1728    fn escape_vscode_linefeed_test() {
1729        let input = "now\nis";
1730        let expected = r#"now\x0Ais"#;
1731        let actual = escape_special_vscode_bytes(input).unwrap();
1732        assert_eq!(expected, actual);
1733    }
1734
1735    #[test]
1736    fn escape_vscode_tab_null_cr_test() {
1737        let input = "now\t\0\ris";
1738        let expected = r#"now\x09\x00\x0Dis"#;
1739        let actual = escape_special_vscode_bytes(input).unwrap();
1740        assert_eq!(expected, actual);
1741    }
1742
1743    #[test]
1744    fn escape_vscode_multibyte_ok() {
1745        let input = "now🍪is";
1746        let actual = escape_special_vscode_bytes(input).unwrap();
1747        assert_eq!(input, actual);
1748    }
1749}