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