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;
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, ShellIntegrationConfig, shell_error};
27use nu_protocol::{
28 Config, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value,
29 config::NuCursorShape,
30 engine::{EngineState, Stack, StateWorkingSet},
31 report_shell_error,
32};
33use nu_utils::time::Instant;
34use nu_utils::{
35 filesystem::{PermissionResult, have_permission},
36 perf, stderr_write_all_and_flush,
37};
38#[cfg(feature = "sqlite")]
39use reedline::SqliteBackedHistory;
40use reedline::{
41 CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
42 HistorySessionId, MouseClickMode, Osc133ClickEventsMarkers, Osc633Markers, Reedline,
43 SemanticPromptMarkers, Vi,
44};
45use std::sync::atomic::Ordering;
46use std::{
47 collections::HashMap,
48 env::temp_dir,
49 io::{self, IsTerminal, Write},
50 panic::{AssertUnwindSafe, catch_unwind},
51 path::{Path, PathBuf},
52 sync::Arc,
53 time::Duration,
54};
55use sysinfo::System;
56
57fn semantic_markers_from_config(
58 config: &Config,
59 term_program_is_vscode: bool,
60) -> Option<Box<dyn SemanticPromptMarkers>> {
61 if config.shell_integration.osc633 && term_program_is_vscode {
62 Some(Osc633Markers::boxed())
63 } else if config.shell_integration.osc133 {
64 Some(Osc133ClickEventsMarkers::boxed())
65 } else {
66 None
67 }
68}
69
70pub fn evaluate_repl(
72 engine_state: &mut EngineState,
73 stack: Stack,
74 prerun_command: Option<Spanned<String>>,
75 load_std_lib: Option<Spanned<String>>,
76 entire_start_time: Instant,
77) -> Result<()> {
78 let mut unique_stack = stack.clone();
84 let config = engine_state.get_config();
85 let use_color = config.use_ansi_coloring.get(engine_state);
86
87 let mut entry_num = 0;
88 let mut is_hostcommand = false;
89
90 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 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 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 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 let mut previous_engine_state = engine_state.clone();
188 let mut previous_stack_arc = Arc::new(unique_stack);
189 loop {
190 let mut current_engine_state = previous_engine_state.clone();
194 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 is_hostcommand: &mut is_hostcommand,
211 });
212
213 (
215 continue_loop,
216 current_engine_state,
217 current_stack,
218 line_editor,
219 )
220 }));
221 match iteration_panic_state {
222 Ok((continue_loop, mut es, s, le)) => {
223 let mut merged_stack = Stack::with_changes_from_child(previous_stack_arc, s);
225
226 let prev_total_vars = previous_engine_state.num_vars();
228 let curr_total_vars = es.num_vars();
229 let new_variables_created = curr_total_vars > prev_total_vars;
230
231 if new_variables_created {
232 es.cleanup_stack_variables(&mut merged_stack);
234 }
235
236 previous_stack_arc = Arc::new(merged_stack);
237 previous_engine_state = es;
239 line_editor = le;
240 if !continue_loop {
241 break;
242 }
243 }
244 Err(_) => {
245 line_editor = get_line_editor(engine_state, use_color)?;
247 }
248 }
249 }
250
251 Ok(())
252}
253
254fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
255 let bytes = input
256 .chars()
257 .flat_map(|c| {
258 let mut buf = [0; 4]; let c_bytes = c.encode_utf8(&mut buf); if c_bytes.len() == 1 {
262 let byte = c_bytes.as_bytes()[0];
263
264 match byte {
265 b if b < 0x20 => format!("\\x{byte:02X}").into_bytes(),
267 b';' => "\\x3B".to_string().into_bytes(),
269 b'\\' => "\\\\".to_string().into_bytes(),
271 _ => vec![byte],
273 }
274 } else {
275 c_bytes.bytes().collect()
277 }
278 })
279 .collect();
280
281 String::from_utf8(bytes).map_err(|err| ShellError::CantConvert {
283 to_type: "string".to_string(),
284 from_type: "bytes".to_string(),
285 span: Span::unknown(),
286 help: Some(format!(
287 "Error {err}, Unable to convert {input} to escaped bytes"
288 )),
289 })
290}
291
292fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
293 let mut start_time = Instant::now();
294 let mut line_editor = Reedline::create();
295
296 store_history_id_in_engine(engine_state, &line_editor);
298 perf!("setup reedline", start_time, use_color);
299
300 if let Some(history) = engine_state.history_config() {
301 start_time = Instant::now();
302
303 line_editor = setup_history(engine_state, line_editor, history)?;
304
305 engine_state.history_locked_after_startup = true;
310
311 perf!("setup history", start_time, use_color);
312 }
313 Ok(line_editor)
314}
315
316struct LoopContext<'a> {
317 engine_state: &'a mut EngineState,
318 stack: Stack,
319 line_editor: Reedline,
320 nu_prompt: &'a mut NushellPrompt,
321 temp_file: &'a Path,
322 use_color: bool,
323 entry_num: &'a mut usize,
324 hostname: Option<&'a str>,
325 is_hostcommand: &'a mut bool,
326}
327
328struct RunContext<'a> {
329 engine_state: &'a mut EngineState,
330 stack: &'a mut Stack,
331 line_editor: Reedline,
332 command: String,
333 hostname: Option<&'a str>,
334 use_color: bool,
335 shell_integration: &'a ShellIntegrationConfig,
336 entry_num: &'a mut usize,
337}
338
339fn run_command(ctx: RunContext) -> Reedline {
340 use nu_cmd_base::hook;
341
342 let RunContext {
343 engine_state,
344 stack,
345 mut line_editor,
346 command,
347 hostname,
348 use_color,
349 shell_integration,
350 entry_num,
351 } = ctx;
352
353 let history_supports_meta = match engine_state.history_config().map(|h| h.file_format) {
354 #[cfg(feature = "sqlite")]
355 Some(HistoryFileFormat::Sqlite) => true,
356 _ => false,
357 };
358
359 if history_supports_meta {
360 prepare_history_metadata(&command, hostname, engine_state, &mut line_editor);
361 }
362
363 let mut start_time = Instant::now();
365
366 {
369 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
371 repl.buffer = command.clone();
372 drop(repl);
373
374 if let Err(err) = hook::eval_hooks(
375 engine_state,
376 stack,
378 vec![],
379 &engine_state.get_config().hooks.pre_execution.clone(),
380 "pre_execution",
381 ) {
382 report_shell_error(None, engine_state, &err);
383 }
384 }
385
386 perf!("pre_execution_hook", start_time, use_color);
387
388 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
389 repl.cursor_pos = line_editor.current_insertion_point();
390 repl.buffer = line_editor.current_buffer_contents().to_string();
391 drop(repl);
392
393 if shell_integration.osc633 {
394 if stack
395 .get_env_var(engine_state, "TERM_PROGRAM")
396 .and_then(|v| v.as_str().ok())
397 == Some("vscode")
398 {
399 start_time = Instant::now();
400
401 run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
402
403 perf!(
404 "pre_execute_marker (633;C) ansi escape sequence",
405 start_time,
406 use_color
407 );
408 } else if shell_integration.osc133 {
409 start_time = Instant::now();
410
411 run_ansi_sequence(PRE_EXECUTION_MARKER);
412
413 perf!(
414 "pre_execute_marker (133;C) ansi escape sequence",
415 start_time,
416 use_color
417 );
418 }
419 } else if shell_integration.osc133 {
420 start_time = Instant::now();
421
422 run_ansi_sequence(PRE_EXECUTION_MARKER);
423
424 perf!(
425 "pre_execute_marker (133;C) ansi escape sequence",
426 start_time,
427 use_color
428 );
429 }
430
431 let cmd_execution_start_time = Instant::now();
433
434 match parse_operation(command.clone(), engine_state, stack) {
435 Ok(ReplOperation::AutoCd { cwd, target, span }) => {
436 do_auto_cd(target, cwd, stack, engine_state, span);
437
438 run_finaliziation_ansi_sequence(
439 stack,
440 engine_state,
441 use_color,
442 shell_integration.osc633,
443 shell_integration.osc133,
444 );
445 }
446 Ok(ReplOperation::RunCommand(cmd)) => {
447 line_editor = do_run_cmd(
448 &cmd,
449 stack,
450 engine_state,
451 line_editor,
452 shell_integration.osc2,
453 *entry_num,
454 use_color,
455 );
456
457 run_finaliziation_ansi_sequence(
458 stack,
459 engine_state,
460 use_color,
461 shell_integration.osc633,
462 shell_integration.osc133,
463 );
464 }
465 Ok(ReplOperation::DoNothing) => {}
467 Err(ref e) => error!("Error parsing operation: {e}"),
468 }
469 let cmd_duration = cmd_execution_start_time.elapsed();
470
471 stack.add_env_var(
473 "CMD_DURATION_MS".into(),
474 Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
475 );
476
477 if history_supports_meta
478 && let Err(e) = fill_in_result_related_history_metadata(
479 &command,
480 engine_state,
481 cmd_duration,
482 stack,
483 &mut line_editor,
484 )
485 {
486 warn!("Could not fill in result related history metadata: {e}");
487 }
488
489 if shell_integration.osc2 {
490 run_shell_integration_osc2(None, engine_state, stack, use_color);
491 }
492 if shell_integration.osc7 {
493 run_shell_integration_osc7(hostname, engine_state, stack, use_color);
494 }
495 if shell_integration.osc9_9 {
496 run_shell_integration_osc9_9(engine_state, stack, use_color);
497 }
498 if shell_integration.osc633 {
499 run_shell_integration_osc633(engine_state, stack, use_color, command);
500 }
501 if shell_integration.reset_application_mode {
502 run_shell_integration_reset_application_mode();
503 }
504
505 line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
506 line_editor
507}
508
509#[inline]
512fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
513 use nu_cmd_base::hook;
514 use reedline::Signal;
515 let loop_start_time = Instant::now();
516
517 let LoopContext {
518 engine_state,
519 mut stack,
520 line_editor,
521 nu_prompt,
522 temp_file,
523 use_color,
524 entry_num,
525 hostname,
526 is_hostcommand,
527 } = ctx;
528
529 let mut start_time = Instant::now();
530 if let Err(err) = engine_state.merge_env(&mut stack) {
533 report_shell_error(None, engine_state, &err);
534 }
535 perf!("merge env", start_time, use_color);
536
537 start_time = Instant::now();
538 engine_state.reset_signals();
539 perf!("reset signals", start_time, use_color);
540
541 start_time = Instant::now();
542
543 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
545 repl.cursor_pos = line_editor.current_insertion_point();
546 repl.buffer = line_editor.current_buffer_contents().to_string();
547 drop(repl);
548
549 if let Err(error) = hook::eval_env_change_hook(
552 &engine_state.get_config().hooks.env_change.clone(),
553 engine_state,
554 &mut stack,
555 ) {
556 report_shell_error(None, engine_state, &error)
557 }
558 perf!("env-change hook", start_time, use_color);
559
560 start_time = Instant::now();
561 if let Err(err) = hook::eval_hooks(
563 engine_state,
564 &mut stack,
565 vec![],
566 &engine_state.get_config().hooks.pre_prompt.clone(),
567 "pre_prompt",
568 ) {
569 report_shell_error(None, engine_state, &err);
570 }
571 perf!("pre-prompt hook", start_time, use_color);
572
573 let engine_reference = Arc::new(engine_state.clone());
574 let config = stack.get_config(engine_state);
575
576 start_time = Instant::now();
577 let cursor_config = CursorConfig {
579 vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
580 vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
581 emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
582 };
583 perf!("get config/cursor config", start_time, use_color);
584
585 start_time = Instant::now();
586 let stack_arc = Arc::new(stack);
590 let term_program_is_vscode = engine_state
591 .get_env_var("TERM_PROGRAM")
592 .and_then(|v| v.as_str().ok())
593 == Some("vscode");
594 let mut line_editor = line_editor
595 .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
596 .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
599 .with_highlighter(Box::new(NuHighlighter::new(
600 engine_reference.clone(),
601 stack_arc.clone(),
603 )))
604 .with_validator(Box::new(NuValidator {
605 engine_state: engine_reference.clone(),
606 }))
607 .with_completer(Box::new(NuCompleter::new(
608 engine_reference.clone(),
609 stack_arc.clone(),
611 )))
612 .with_quick_completions(config.completions.quick)
613 .with_partial_completions(config.completions.partial)
614 .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
615 .with_cwd(Some(
616 engine_state
617 .cwd(None)
618 .map(|cwd| cwd.into_std_path_buf())
619 .unwrap_or_default()
620 .to_string_lossy()
621 .to_string(),
622 ))
623 .with_cursor_config(cursor_config)
624 .with_abbreviations(config.abbreviations.clone())
625 .with_visual_selection_style(nu_ansi_term::Style {
626 is_reverse: true,
627 ..Default::default()
628 })
629 .with_semantic_markers(semantic_markers_from_config(
630 &config,
631 term_program_is_vscode,
632 ))
633 .with_mouse_click(if config.shell_integration.osc133 {
634 MouseClickMode::Enabled
635 } else {
636 MouseClickMode::Disabled
637 });
638
639 perf!("reedline builder", start_time, use_color);
640
641 let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
642
643 start_time = Instant::now();
644 line_editor = if config.use_ansi_coloring.get(engine_state) && config.show_hints {
645 let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
648 if let Some(closure) = config.hinter.closure.as_ref() {
649 line_editor.with_hinter(Box::new(ExternalHinter::new(
650 engine_reference.clone(),
651 stack_arc.clone(),
652 closure.clone(),
653 style,
654 )))
655 } else {
656 line_editor.with_hinter(Box::new(CwdAwareHinter::default().with_style(style)))
657 }
658 } else {
659 line_editor.disable_hints()
660 };
661
662 perf!("reedline coloring/style_computer", start_time, use_color);
663
664 start_time = Instant::now();
665 trace!("adding menus");
666 line_editor =
667 add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
668 report_shell_error(None, engine_state, &e);
669 Reedline::create()
670 });
671
672 perf!("reedline adding menus", start_time, use_color);
673
674 start_time = Instant::now();
675 let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
677
678 line_editor = if let Ok((cmd, args)) = buffer_editor {
679 let mut command = std::process::Command::new(cmd);
680 let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
681 warn!("Couldn't convert environment variable values to strings: {e}");
682 HashMap::default()
683 });
684 command.args(args).envs(envs);
685 line_editor.with_buffer_editor(command, temp_file.to_path_buf())
686 } else {
687 line_editor
688 };
689
690 perf!("reedline buffer_editor", start_time, use_color);
691
692 if let Some(history) = engine_state.history_config() {
693 start_time = Instant::now();
694
695 line_editor = line_editor
696 .with_history_exclusion_prefix(history.ignore_space_prefixed.then_some(" ".into()));
697
698 if history.sync_on_enter
699 && let Err(e) = line_editor.sync_history()
700 {
701 warn!("Failed to sync history: {e}");
702 }
703
704 perf!("sync_history", start_time, use_color);
705 }
706
707 start_time = Instant::now();
708 line_editor = setup_keybindings(engine_state, line_editor);
710
711 perf!("keybindings", start_time, use_color);
712
713 start_time = Instant::now();
714 let config = &engine_state.get_config().clone();
715 prompt_update::update_prompt(
716 config,
717 engine_state,
718 &mut Stack::with_parent(stack_arc.clone()),
719 nu_prompt,
720 );
721 let transient_prompt = prompt_update::make_transient_prompt(
722 config,
723 engine_state,
724 &mut Stack::with_parent(stack_arc.clone()),
725 nu_prompt,
726 );
727
728 perf!("update_prompt", start_time, use_color);
729
730 if !*is_hostcommand {
735 line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
736 }
737 *is_hostcommand = false;
738
739 *entry_num += 1;
740
741 start_time = Instant::now();
742 line_editor = line_editor.with_transient_prompt(transient_prompt);
743 let input = line_editor.read_line(nu_prompt);
744 line_editor = line_editor
747 .with_highlighter(Box::<NoOpHighlighter>::default())
749 .with_completer(Box::<DefaultCompleter>::default())
751 .with_immediately_accept(false);
753
754 let shell_integration = &config.shell_integration;
755
756 let mut stack = Arc::unwrap_or_clone(stack_arc);
759
760 perf!("line_editor setup", start_time, use_color);
761
762 let line_editor_input_time = Instant::now();
763 match input {
764 Ok(Signal::Success(command)) => {
765 line_editor = run_command(RunContext {
766 engine_state,
767 stack: &mut stack,
768 line_editor,
769 command,
770 hostname,
771 use_color,
772 shell_integration,
773 entry_num,
774 });
775 }
776 Ok(Signal::HostCommand(command)) => {
777 *is_hostcommand = true;
778 line_editor = run_command(RunContext {
779 engine_state,
780 stack: &mut stack,
781 line_editor,
782 command,
783 hostname,
784 use_color,
785 shell_integration,
786 entry_num,
787 });
788 }
789 Ok(Signal::CtrlC) => {
790 run_finaliziation_ansi_sequence(
792 &stack,
793 engine_state,
794 use_color,
795 shell_integration.osc633,
796 shell_integration.osc133,
797 );
798 }
799 Ok(Signal::CtrlD) => {
800 run_finaliziation_ansi_sequence(
803 &stack,
804 engine_state,
805 use_color,
806 shell_integration.osc633,
807 shell_integration.osc133,
808 );
809
810 println!();
811
812 cleanup_exit((), engine_state, 0);
813
814 return (true, stack, line_editor);
816 }
817 Ok(_) => {}
819 Err(err) => {
820 if !err.to_string().contains("duration") {
821 write_repl_error_details(&err);
822 cleanup_exit((), engine_state, 1);
823 return (true, stack, line_editor);
824 }
825
826 run_finaliziation_ansi_sequence(
827 &stack,
828 engine_state,
829 use_color,
830 shell_integration.osc633,
831 shell_integration.osc133,
832 );
833 }
834 }
835 perf!(
836 "processing line editor input",
837 line_editor_input_time,
838 use_color
839 );
840
841 perf!(
842 "time between prompts in line editor loop",
843 loop_start_time,
844 use_color
845 );
846
847 (true, stack, line_editor)
848}
849
850fn prepare_history_metadata(
854 s: &str,
855 hostname: Option<&str>,
856 engine_state: &EngineState,
857 line_editor: &mut Reedline,
858) {
859 if !s.is_empty() && line_editor.has_last_command_context() {
860 let result = line_editor
861 .update_last_command_context(&|mut c| {
862 c.start_timestamp = Some(chrono::Utc::now());
863 c.hostname = hostname.map(str::to_string);
864 c.cwd = engine_state
865 .cwd(None)
866 .ok()
867 .map(|path| path.to_string_lossy().to_string());
868 c
869 })
870 .into_diagnostic();
871 if let Err(e) = result {
872 warn!("Could not prepare history metadata: {e}");
873 }
874 }
875}
876
877fn fill_in_result_related_history_metadata(
881 s: &str,
882 engine_state: &EngineState,
883 cmd_duration: Duration,
884 stack: &mut Stack,
885 line_editor: &mut Reedline,
886) -> Result<()> {
887 if !s.is_empty() && line_editor.has_last_command_context() {
888 line_editor
889 .update_last_command_context(&|mut c| {
890 c.duration = Some(cmd_duration);
891 c.exit_status = stack
892 .get_env_var(engine_state, "LAST_EXIT_CODE")
893 .and_then(|e| e.as_int().ok());
894 c
895 })
896 .into_diagnostic()?; }
898 Ok(())
899}
900
901enum ReplOperation {
903 AutoCd {
905 cwd: String,
907 target: PathBuf,
909 span: Span,
911 },
912 RunCommand(String),
914 DoNothing,
916}
917
918fn parse_operation(
926 s: String,
927 engine_state: &EngineState,
928 stack: &Stack,
929) -> Result<ReplOperation, ErrReport> {
930 let tokens = lex(s.as_bytes(), 0, &[], &[], false);
931 let cwd = engine_state
933 .cwd(Some(stack))
934 .map(|p| p.to_string_lossy().to_string())
935 .unwrap_or_default();
936 let mut orig = s.trim().to_string();
937 if orig.starts_with('`') {
938 orig = trim_quotes_str(&orig).to_string()
939 }
940
941 let path = nu_path::expand_path_with(&orig, &cwd, true);
942 if (engine_state.get_config().auto_cd_implicit || looks_like_path(&orig))
943 && path.is_dir()
944 && tokens.0.len() == 1
945 {
946 Ok(ReplOperation::AutoCd {
947 cwd,
948 target: path,
949 span: tokens.0[0].span,
950 })
951 } else if !s.trim().is_empty() {
952 Ok(ReplOperation::RunCommand(s))
953 } else {
954 Ok(ReplOperation::DoNothing)
955 }
956}
957
958fn do_auto_cd(
962 path: PathBuf,
963 cwd: String,
964 stack: &mut Stack,
965 engine_state: &mut EngineState,
966 span: Span,
967) {
968 let path = {
969 if !path.exists() {
970 report_shell_error(
971 Some(stack),
972 engine_state,
973 &ShellError::Io(IoError::new_with_additional_context(
974 shell_error::io::ErrorKind::DirectoryNotFound,
975 span,
976 PathBuf::from(&path),
977 "Cannot change directory",
978 )),
979 );
980 }
981 path.to_string_lossy().to_string()
982 };
983
984 if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
985 report_shell_error(
986 Some(stack),
987 engine_state,
988 &ShellError::Io(IoError::new_with_additional_context(
989 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
990 span,
991 PathBuf::from(path),
992 "Cannot change directory",
993 )),
994 );
995 return;
996 }
997
998 stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), span));
999
1000 if let Err(err) = stack.set_cwd(&path) {
1003 report_shell_error(Some(stack), engine_state, &err);
1004 return;
1005 };
1006 let cwd = Value::string(cwd, span);
1007
1008 let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
1009 let mut shells = if let Some(v) = shells {
1010 v.clone().into_list().unwrap_or_else(|_| vec![cwd])
1011 } else {
1012 vec![cwd]
1013 };
1014
1015 let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
1016 let current_shell = if let Some(v) = current_shell {
1017 v.as_int().unwrap_or_default() as usize
1018 } else {
1019 0
1020 };
1021
1022 let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
1023 let last_shell = if let Some(v) = last_shell {
1024 v.as_int().unwrap_or_default() as usize
1025 } else {
1026 0
1027 };
1028
1029 shells[current_shell] = Value::string(path, span);
1030
1031 stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
1032 stack.add_env_var(
1033 "NUSHELL_LAST_SHELL".into(),
1034 Value::int(last_shell as i64, span),
1035 );
1036 stack.set_last_exit_code(0, span);
1037}
1038
1039fn do_run_cmd(
1044 s: &str,
1045 stack: &mut Stack,
1046 engine_state: &mut EngineState,
1047 line_editor: Reedline,
1050 shell_integration_osc2: bool,
1051 entry_num: usize,
1052 use_color: bool,
1053) -> Reedline {
1054 trace!("eval source: {s}");
1055
1056 let mut cmds = s.split_whitespace();
1057
1058 let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
1059
1060 if let Some("exit") = cmds.next() {
1061 let mut working_set = StateWorkingSet::new(engine_state);
1062 let _ = parse(&mut working_set, None, s.as_bytes(), false);
1063
1064 if working_set.parse_errors.is_empty() {
1065 match cmds.next() {
1066 Some(s) => {
1067 if let Ok(n) = s.parse::<i32>() {
1068 return cleanup_exit(line_editor, engine_state, n);
1069 }
1070 }
1071 None => {
1072 return cleanup_exit(line_editor, engine_state, 0);
1073 }
1074 }
1075 }
1076 }
1077
1078 if shell_integration_osc2 {
1079 run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
1080 }
1081
1082 eval_source(
1083 engine_state,
1084 stack,
1085 s.as_bytes(),
1086 &format!("repl_entry #{entry_num}"),
1087 PipelineData::empty(),
1088 false,
1089 );
1090
1091 if had_warning_before && engine_state.is_interactive {
1094 engine_state
1095 .exit_warning_given
1096 .store(false, Ordering::SeqCst);
1097 }
1098
1099 line_editor
1100}
1101
1102fn run_shell_integration_osc2(
1108 command_name: Option<&str>,
1109 engine_state: &EngineState,
1110 stack: &mut Stack,
1111 use_color: bool,
1112) {
1113 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1114 let start_time = Instant::now();
1115
1116 let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1118 let home_dir_str = p.as_path().display().to_string();
1119 if path.starts_with(&home_dir_str) {
1120 path.replacen(&home_dir_str, "~", 1)
1121 } else {
1122 path
1123 }
1124 } else {
1125 path
1126 };
1127
1128 let title = match command_name {
1129 Some(binary_name) => {
1130 let split_binary_name = binary_name.split_whitespace().next();
1131 if let Some(binary_name) = split_binary_name {
1132 format!("{maybe_abbrev_path}> {binary_name}")
1133 } else {
1134 maybe_abbrev_path.to_string()
1135 }
1136 }
1137 None => maybe_abbrev_path.to_string(),
1138 };
1139
1140 run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1146
1147 perf!("set title with command osc2", start_time, use_color);
1148 }
1149}
1150
1151fn run_shell_integration_osc7(
1152 hostname: Option<&str>,
1153 engine_state: &EngineState,
1154 stack: &mut Stack,
1155 use_color: bool,
1156) {
1157 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1158 let start_time = Instant::now();
1159
1160 let path = if cfg!(windows) {
1161 path.replace('\\', "/")
1162 } else {
1163 path
1164 };
1165
1166 run_ansi_sequence(&format!(
1168 "\x1b]7;file://{}{}{}\x1b\\",
1169 percent_encoding::utf8_percent_encode(
1170 hostname.unwrap_or("localhost"),
1171 percent_encoding::CONTROLS
1172 ),
1173 if path.starts_with('/') { "" } else { "/" },
1174 percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1175 ));
1176
1177 perf!(
1178 "communicate path to terminal with osc7",
1179 start_time,
1180 use_color
1181 );
1182 }
1183}
1184
1185fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1186 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1187 let start_time = Instant::now();
1188
1189 run_ansi_sequence(&format!("\x1b]9;9;{}\x1b\\", path));
1192
1193 perf!(
1194 "communicate path to terminal with osc9;9",
1195 start_time,
1196 use_color
1197 );
1198 }
1199}
1200
1201fn run_shell_integration_osc633(
1202 engine_state: &EngineState,
1203 stack: &mut Stack,
1204 use_color: bool,
1205 repl_cmd_line_text: String,
1206) {
1207 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1208 if stack
1211 .get_env_var(engine_state, "TERM_PROGRAM")
1212 .and_then(|v| v.as_str().ok())
1213 == Some("vscode")
1214 {
1215 let start_time = Instant::now();
1216
1217 run_ansi_sequence(&format!(
1220 "{VSCODE_CWD_PROPERTY_MARKER_PREFIX}{path}{VSCODE_CWD_PROPERTY_MARKER_SUFFIX}"
1221 ));
1222
1223 perf!(
1224 "communicate path to terminal with osc633;P",
1225 start_time,
1226 use_color
1227 );
1228
1229 let replaced_cmd_text =
1232 escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1233
1234 run_ansi_sequence(&format!(
1236 "{VSCODE_COMMANDLINE_MARKER_PREFIX}{replaced_cmd_text}{VSCODE_COMMANDLINE_MARKER_SUFFIX}"
1237 ));
1238 }
1239 }
1240}
1241
1242fn run_shell_integration_reset_application_mode() {
1243 run_ansi_sequence(RESET_APPLICATION_MODE);
1244}
1245
1246fn flush_engine_state_repl_buffer(
1250 engine_state: &mut EngineState,
1251 mut line_editor: Reedline,
1252) -> Reedline {
1253 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1254 line_editor.run_edit_commands(&[
1255 EditCommand::Clear,
1256 EditCommand::InsertString(repl.buffer.to_string()),
1257 EditCommand::MoveToPosition {
1258 position: repl.cursor_pos,
1259 select: false,
1260 },
1261 ]);
1262 if repl.accept {
1263 line_editor = line_editor.with_immediately_accept(true)
1264 }
1265 repl.accept = false;
1266 repl.buffer = "".to_string();
1267 repl.cursor_pos = 0;
1268 line_editor
1269}
1270
1271fn setup_history(
1275 engine_state: &mut EngineState,
1276 line_editor: Reedline,
1277 history: HistoryConfig,
1278) -> Result<Reedline> {
1279 let history_session_id = if history.isolation {
1281 Reedline::create_history_session_id()
1282 } else {
1283 None
1284 };
1285
1286 if let Some(path) = history.file_path() {
1287 return update_line_editor_history(
1288 engine_state,
1289 path,
1290 history,
1291 line_editor,
1292 history_session_id,
1293 );
1294 };
1295 Ok(line_editor)
1296}
1297
1298fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1302 match create_keybindings(engine_state.get_config()) {
1303 Ok(keybindings) => match keybindings {
1304 KeybindingsMode::Emacs(keybindings) => {
1305 let edit_mode = Box::new(Emacs::new(keybindings));
1306 line_editor.with_edit_mode(edit_mode)
1307 }
1308 KeybindingsMode::Vi {
1309 insert_keybindings,
1310 normal_keybindings,
1311 } => {
1312 let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1313 line_editor.with_edit_mode(edit_mode)
1314 }
1315 },
1316 Err(e) => {
1317 report_shell_error(None, engine_state, &e);
1318 line_editor
1319 }
1320 }
1321}
1322
1323fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1327 if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1328 warn!("Terminal doesn't support use_kitty_protocol config");
1329 }
1330}
1331
1332fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1333 let session_id = line_editor
1334 .get_history_session_id()
1335 .map(i64::from)
1336 .unwrap_or(0);
1337
1338 engine_state.history_session_id = session_id;
1339}
1340
1341fn update_line_editor_history(
1342 engine_state: &mut EngineState,
1343 history_path: PathBuf,
1344 history: HistoryConfig,
1345 line_editor: Reedline,
1346 history_session_id: Option<HistorySessionId>,
1347) -> Result<Reedline, ErrReport> {
1348 let ignore_space_prefixed = history.ignore_space_prefixed;
1349 let history: Box<dyn reedline::History> = match history.file_format {
1350 HistoryFileFormat::Plaintext => Box::new(
1351 FileBackedHistory::with_file(history.max_size as usize, history_path)
1352 .into_diagnostic()?,
1353 ),
1354 #[cfg(not(feature = "sqlite"))]
1356 HistoryFileFormat::Sqlite => {
1357 return Err(miette::miette!(
1358 help = "compile Nushell with the `sqlite` feature to use this",
1359 "Unsupported history file format",
1360 ));
1361 }
1362 #[cfg(feature = "sqlite")]
1363 HistoryFileFormat::Sqlite => Box::new(
1364 SqliteBackedHistory::with_file(
1365 history_path.to_path_buf(),
1366 history_session_id,
1367 Some(chrono::Utc::now()),
1368 )
1369 .into_diagnostic()?,
1370 ),
1371 };
1372 let line_editor = line_editor
1373 .with_history_session_id(history_session_id)
1374 .with_history_exclusion_prefix(ignore_space_prefixed.then_some(" ".into()))
1375 .with_history(history);
1376
1377 store_history_id_in_engine(engine_state, &line_editor);
1378
1379 Ok(line_editor)
1380}
1381
1382fn confirm_stdin_is_terminal() -> Result<()> {
1383 if !std::io::stdin().is_terminal() {
1386 return Err(std::io::Error::new(
1387 std::io::ErrorKind::NotFound,
1388 "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1389 ))
1390 .into_diagnostic();
1391 }
1392 Ok(())
1393}
1394fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1395 match shape {
1396 NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1397 NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1398 NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1399 NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1400 NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1401 NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1402 NuCursorShape::Inherit => None,
1403 }
1404}
1405
1406fn get_command_finished_marker(
1407 stack: &Stack,
1408 engine_state: &EngineState,
1409 shell_integration_osc633: bool,
1410 shell_integration_osc133: bool,
1411) -> String {
1412 let exit_code = stack
1413 .get_env_var(engine_state, "LAST_EXIT_CODE")
1414 .and_then(|e| e.as_int().ok());
1415
1416 if shell_integration_osc633 {
1417 if stack
1418 .get_env_var(engine_state, "TERM_PROGRAM")
1419 .and_then(|v| v.as_str().ok())
1420 == Some("vscode")
1421 {
1422 format!(
1424 "{}{}{}",
1425 VSCODE_POST_EXECUTION_MARKER_PREFIX,
1426 exit_code.unwrap_or(0),
1427 VSCODE_POST_EXECUTION_MARKER_SUFFIX
1428 )
1429 } else if shell_integration_osc133 {
1430 format!(
1432 "{}{}{}",
1433 POST_EXECUTION_MARKER_PREFIX,
1434 exit_code.unwrap_or(0),
1435 POST_EXECUTION_MARKER_SUFFIX
1436 )
1437 } else {
1438 "\x1b[0m".to_string()
1440 }
1441 } else if shell_integration_osc133 {
1442 format!(
1443 "{}{}{}",
1444 POST_EXECUTION_MARKER_PREFIX,
1445 exit_code.unwrap_or(0),
1446 POST_EXECUTION_MARKER_SUFFIX
1447 )
1448 } else {
1449 "\x1b[0m".to_string()
1450 }
1451}
1452
1453fn run_ansi_sequence(seq: &str) {
1454 if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1455 warn!("Error writing ansi sequence {e}");
1456 } else if let Err(e) = io::stdout().flush() {
1457 warn!("Error flushing stdio {e}");
1458 }
1459}
1460
1461fn write_repl_error_details(error: &impl std::fmt::Debug) {
1462 let _ = stderr_write_all_and_flush(format!("Error: {error:?}\n"));
1463}
1464
1465fn run_finaliziation_ansi_sequence(
1466 stack: &Stack,
1467 engine_state: &EngineState,
1468 use_color: bool,
1469 shell_integration_osc633: bool,
1470 shell_integration_osc133: bool,
1471) {
1472 if shell_integration_osc633 {
1473 if stack
1475 .get_env_var(engine_state, "TERM_PROGRAM")
1476 .and_then(|v| v.as_str().ok())
1477 == Some("vscode")
1478 {
1479 let start_time = Instant::now();
1480
1481 run_ansi_sequence(&get_command_finished_marker(
1482 stack,
1483 engine_state,
1484 shell_integration_osc633,
1485 shell_integration_osc133,
1486 ));
1487
1488 perf!(
1489 "post_execute_marker (633;D) ansi escape sequences",
1490 start_time,
1491 use_color
1492 );
1493 } else if shell_integration_osc133 {
1494 let start_time = Instant::now();
1495
1496 run_ansi_sequence(&get_command_finished_marker(
1497 stack,
1498 engine_state,
1499 shell_integration_osc633,
1500 shell_integration_osc133,
1501 ));
1502
1503 perf!(
1504 "post_execute_marker (133;D) ansi escape sequences",
1505 start_time,
1506 use_color
1507 );
1508 }
1509 } else if shell_integration_osc133 {
1510 let start_time = Instant::now();
1511
1512 run_ansi_sequence(&get_command_finished_marker(
1513 stack,
1514 engine_state,
1515 shell_integration_osc633,
1516 shell_integration_osc133,
1517 ));
1518
1519 perf!(
1520 "post_execute_marker (133;D) ansi escape sequences",
1521 start_time,
1522 use_color
1523 );
1524 }
1525}
1526
1527#[cfg(windows)]
1529static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1530 fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1531});
1532
1533fn looks_like_path(orig: &str) -> bool {
1535 #[cfg(windows)]
1536 {
1537 if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1538 return true;
1539 }
1540 }
1541
1542 orig.starts_with('.')
1543 || orig.starts_with('~')
1544 || orig.starts_with('/')
1545 || orig.starts_with('\\')
1546 || orig.ends_with(std::path::MAIN_SEPARATOR)
1547}
1548
1549#[cfg(test)]
1550mod semantic_marker_tests {
1551 use super::semantic_markers_from_config;
1552 use nu_protocol::Config;
1553 use reedline::PromptKind;
1554
1555 #[test]
1556 fn semantic_markers_use_osc633_in_vscode() {
1557 let mut config = Config::default();
1558 config.shell_integration.osc633 = true;
1559 config.shell_integration.osc133 = true;
1560
1561 let markers =
1562 semantic_markers_from_config(&config, true).expect("expected semantic markers");
1563
1564 assert_eq!(
1565 markers.prompt_start(PromptKind::Primary).as_ref(),
1566 "\x1b]633;A;k=i\x1b\\"
1567 );
1568 }
1569
1570 #[test]
1571 fn semantic_markers_use_osc133_click_events() {
1572 let mut config = Config::default();
1573 config.shell_integration.osc133 = true;
1574
1575 let markers =
1576 semantic_markers_from_config(&config, false).expect("expected semantic markers");
1577
1578 assert_eq!(
1579 markers.prompt_start(PromptKind::Primary).as_ref(),
1580 "\x1b]133;A;k=i;click_events=1\x1b\\"
1581 );
1582 }
1583
1584 #[test]
1585 fn semantic_markers_none_when_disabled() {
1586 let mut config = Config::default();
1587 config.shell_integration.osc133 = false;
1588 config.shell_integration.osc633 = false;
1589 assert!(semantic_markers_from_config(&config, false).is_none());
1590 }
1591}
1592
1593#[cfg(windows)]
1594#[test]
1595fn looks_like_path_windows_drive_path_works() {
1596 assert!(looks_like_path("C:"));
1597 assert!(looks_like_path("D:\\"));
1598 assert!(looks_like_path("E:/"));
1599 assert!(looks_like_path("F:\\some_dir"));
1600 assert!(looks_like_path("G:/some_dir"));
1601}
1602
1603#[cfg(windows)]
1604#[test]
1605fn trailing_slash_looks_like_path() {
1606 assert!(looks_like_path("foo\\"))
1607}
1608
1609#[cfg(not(windows))]
1610#[test]
1611fn trailing_slash_looks_like_path() {
1612 assert!(looks_like_path("foo/"))
1613}
1614
1615#[test]
1616fn are_session_ids_in_sync() {
1617 let engine_state = &mut EngineState::new();
1618 let history = engine_state.history_config().unwrap();
1619 let history_path = history.file_path().unwrap();
1620 let line_editor = reedline::Reedline::create();
1621 let history_session_id = reedline::Reedline::create_history_session_id();
1622 let line_editor = update_line_editor_history(
1623 engine_state,
1624 history_path,
1625 history,
1626 line_editor,
1627 history_session_id,
1628 );
1629 assert_eq!(
1630 i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1631 engine_state.history_session_id
1632 );
1633}
1634
1635#[cfg(test)]
1636mod test_auto_cd {
1637 use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1638 use nu_path::AbsolutePath;
1639 use nu_protocol::engine::{EngineState, Stack};
1640 use tempfile::tempdir;
1641
1642 #[cfg(any(unix, windows))]
1644 fn symlink(
1645 original: impl AsRef<AbsolutePath>,
1646 link: impl AsRef<AbsolutePath>,
1647 ) -> std::io::Result<()> {
1648 let original = original.as_ref();
1649 let link = link.as_ref();
1650
1651 #[cfg(unix)]
1652 {
1653 std::os::unix::fs::symlink(original, link)
1654 }
1655 #[cfg(windows)]
1656 {
1657 if original.is_dir() {
1658 std::os::windows::fs::symlink_dir(original, link)
1659 } else {
1660 std::os::windows::fs::symlink_file(original, link)
1661 }
1662 }
1663 }
1664
1665 #[track_caller]
1669 fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1670 let mut engine_state = EngineState::new();
1672 let mut stack = Stack::new();
1673 stack.set_cwd(before.as_ref()).unwrap();
1674
1675 let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1677 let ReplOperation::AutoCd { cwd, target, span } = op else {
1678 panic!("'{input}' was not parsed into an auto-cd operation")
1679 };
1680
1681 do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1683 let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1684
1685 let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1689 let after = std::fs::canonicalize(after.as_ref()).unwrap();
1690 assert_eq!(updated_cwd, after);
1691 }
1692
1693 #[test]
1694 fn auto_cd_root() {
1695 let tempdir = tempdir().unwrap();
1696 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1697
1698 let input = if cfg!(windows) { r"C:\" } else { "/" };
1699 let root = AbsolutePath::try_new(input).unwrap();
1700 check(tempdir, input, root);
1701 }
1702
1703 #[test]
1704 fn auto_cd_tilde() {
1705 let tempdir = tempdir().unwrap();
1706 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1707
1708 let home = nu_path::home_dir().unwrap();
1709 check(tempdir, "~", home);
1710 }
1711
1712 #[test]
1713 fn auto_cd_dot() {
1714 let tempdir = tempdir().unwrap();
1715 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1716
1717 check(tempdir, ".", tempdir);
1718 }
1719
1720 #[test]
1721 fn auto_cd_double_dot() {
1722 let tempdir = tempdir().unwrap();
1723 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1724
1725 let dir = tempdir.join("foo");
1726 std::fs::create_dir_all(&dir).unwrap();
1727 check(dir, "..", tempdir);
1728 }
1729
1730 #[test]
1731 fn auto_cd_triple_dot() {
1732 let tempdir = tempdir().unwrap();
1733 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1734
1735 let dir = tempdir.join("foo").join("bar");
1736 std::fs::create_dir_all(&dir).unwrap();
1737 check(dir, "...", tempdir);
1738 }
1739
1740 #[test]
1741 fn auto_cd_relative() {
1742 let tempdir = tempdir().unwrap();
1743 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1744
1745 let foo = tempdir.join("foo");
1746 let bar = tempdir.join("bar");
1747 std::fs::create_dir_all(&foo).unwrap();
1748 std::fs::create_dir_all(&bar).unwrap();
1749 let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1750 check(foo, input, bar);
1751 }
1752
1753 #[test]
1754 fn auto_cd_trailing_slash() {
1755 let tempdir = tempdir().unwrap();
1756 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1757
1758 let dir = tempdir.join("foo");
1759 std::fs::create_dir_all(&dir).unwrap();
1760 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1761 check(tempdir, input, dir);
1762 }
1763
1764 #[test]
1765 fn auto_cd_symlink() {
1766 let tempdir = tempdir().unwrap();
1767 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1768
1769 let dir = tempdir.join("foo");
1770 std::fs::create_dir_all(&dir).unwrap();
1771 let link = tempdir.join("link");
1772 symlink(&dir, &link).unwrap();
1773 let input = if cfg!(windows) { r".\link" } else { "./link" };
1774 check(tempdir, input, link);
1775
1776 let dir = tempdir.join("foo").join("bar");
1777 std::fs::create_dir_all(&dir).unwrap();
1778 let link = tempdir.join("link2");
1779 symlink(&dir, &link).unwrap();
1780 let input = "..";
1781 check(link, input, tempdir);
1782 }
1783
1784 #[test]
1785 #[should_panic(expected = "was not parsed into an auto-cd operation")]
1786 fn auto_cd_nonexistent_directory() {
1787 let tempdir = tempdir().unwrap();
1788 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1789
1790 let dir = tempdir.join("foo");
1791 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1792 check(tempdir, input, dir);
1793 }
1794
1795 #[test]
1796 fn escape_vscode_semicolon_test() {
1797 let input = "now;is";
1798 let expected = r#"now\x3Bis"#;
1799 let actual = escape_special_vscode_bytes(input).unwrap();
1800 assert_eq!(expected, actual);
1801 }
1802
1803 #[test]
1804 fn escape_vscode_backslash_test() {
1805 let input = r#"now\is"#;
1806 let expected = r#"now\\is"#;
1807 let actual = escape_special_vscode_bytes(input).unwrap();
1808 assert_eq!(expected, actual);
1809 }
1810
1811 #[test]
1812 fn escape_vscode_linefeed_test() {
1813 let input = "now\nis";
1814 let expected = r#"now\x0Ais"#;
1815 let actual = escape_special_vscode_bytes(input).unwrap();
1816 assert_eq!(expected, actual);
1817 }
1818
1819 #[test]
1820 fn escape_vscode_tab_null_cr_test() {
1821 let input = "now\t\0\ris";
1822 let expected = r#"now\x09\x00\x0Dis"#;
1823 let actual = escape_special_vscode_bytes(input).unwrap();
1824 assert_eq!(expected, actual);
1825 }
1826
1827 #[test]
1828 fn escape_vscode_multibyte_ok() {
1829 let input = "now🍪is";
1830 let actual = escape_special_vscode_bytes(input).unwrap();
1831 assert_eq!(input, actual);
1832 }
1833}