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