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