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