Skip to main content

nu_cli/
repl.rs

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