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 if let Err(error) = hook::eval_env_change_hook(
545 &engine_state.get_config().hooks.env_change.clone(),
546 engine_state,
547 &mut stack,
548 ) {
549 report_shell_error(None, engine_state, &error)
550 }
551 perf!("env-change hook", start_time, use_color);
552
553 start_time = Instant::now();
554 if let Err(err) = hook::eval_hooks(
556 engine_state,
557 &mut stack,
558 vec![],
559 &engine_state.get_config().hooks.pre_prompt.clone(),
560 "pre_prompt",
561 ) {
562 report_shell_error(None, engine_state, &err);
563 }
564 perf!("pre-prompt hook", start_time, use_color);
565
566 let engine_reference = Arc::new(engine_state.clone());
567 let config = stack.get_config(engine_state);
568
569 start_time = Instant::now();
570 let cursor_config = CursorConfig {
572 vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
573 vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
574 emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
575 };
576 perf!("get config/cursor config", start_time, use_color);
577
578 start_time = Instant::now();
579 let stack_arc = Arc::new(stack);
583 let term_program_is_vscode = engine_state
584 .get_env_var("TERM_PROGRAM")
585 .and_then(|v| v.as_str().ok())
586 == Some("vscode");
587 let mut line_editor = line_editor
588 .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
589 .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
592 .with_highlighter(Box::new(NuHighlighter::new(
593 engine_reference.clone(),
594 stack_arc.clone(),
596 )))
597 .with_validator(Box::new(NuValidator {
598 engine_state: engine_reference.clone(),
599 }))
600 .with_completer(Box::new(NuCompleter::new(
601 engine_reference.clone(),
602 stack_arc.clone(),
604 )))
605 .with_quick_completions(config.completions.quick)
606 .with_partial_completions(config.completions.partial)
607 .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
608 .with_cwd(Some(
609 engine_state
610 .cwd(None)
611 .map(|cwd| cwd.into_std_path_buf())
612 .unwrap_or_default()
613 .to_string_lossy()
614 .to_string(),
615 ))
616 .with_cursor_config(cursor_config)
617 .with_abbreviations(config.abbreviations.clone())
618 .with_visual_selection_style(nu_ansi_term::Style {
619 is_reverse: true,
620 ..Default::default()
621 })
622 .with_semantic_markers(semantic_markers_from_config(
623 &config,
624 term_program_is_vscode,
625 ))
626 .with_mouse_click(if config.shell_integration.osc133 {
627 MouseClickMode::Enabled
628 } else {
629 MouseClickMode::Disabled
630 });
631
632 perf!("reedline builder", start_time, use_color);
633
634 let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
635
636 start_time = Instant::now();
637 line_editor = if config.use_ansi_coloring.get(engine_state) && config.show_hints {
638 let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
641 if let Some(closure) = config.hinter.closure.as_ref() {
642 line_editor.with_hinter(Box::new(ExternalHinter::new(
643 engine_reference.clone(),
644 stack_arc.clone(),
645 closure.clone(),
646 style,
647 )))
648 } else {
649 line_editor.with_hinter(Box::new(CwdAwareHinter::default().with_style(style)))
650 }
651 } else {
652 line_editor.disable_hints()
653 };
654
655 perf!("reedline coloring/style_computer", start_time, use_color);
656
657 start_time = Instant::now();
658 trace!("adding menus");
659 line_editor =
660 add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
661 report_shell_error(None, engine_state, &e);
662 Reedline::create()
663 });
664
665 perf!("reedline adding menus", start_time, use_color);
666
667 start_time = Instant::now();
668 let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
670
671 line_editor = if let Ok((cmd, args)) = buffer_editor {
672 let mut command = std::process::Command::new(cmd);
673 let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
674 warn!("Couldn't convert environment variable values to strings: {e}");
675 HashMap::default()
676 });
677 command.args(args).envs(envs);
678 line_editor.with_buffer_editor(command, temp_file.to_path_buf())
679 } else {
680 line_editor
681 };
682
683 perf!("reedline buffer_editor", start_time, use_color);
684
685 if let Some(history) = engine_state.history_config() {
686 start_time = Instant::now();
687
688 line_editor = line_editor
689 .with_history_exclusion_prefix(history.ignore_space_prefixed.then_some(" ".into()));
690
691 if history.sync_on_enter
692 && let Err(e) = line_editor.sync_history()
693 {
694 warn!("Failed to sync history: {e}");
695 }
696
697 perf!("sync_history", start_time, use_color);
698 }
699
700 start_time = Instant::now();
701 line_editor = setup_keybindings(engine_state, line_editor);
703
704 perf!("keybindings", start_time, use_color);
705
706 start_time = Instant::now();
707 let config = &engine_state.get_config().clone();
708 prompt_update::update_prompt(
709 config,
710 engine_state,
711 &mut Stack::with_parent(stack_arc.clone()),
712 nu_prompt,
713 );
714 let transient_prompt = prompt_update::make_transient_prompt(
715 config,
716 engine_state,
717 &mut Stack::with_parent(stack_arc.clone()),
718 nu_prompt,
719 );
720
721 perf!("update_prompt", start_time, use_color);
722
723 if !*is_hostcommand {
728 line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
729 }
730 *is_hostcommand = false;
731
732 *entry_num += 1;
733
734 start_time = Instant::now();
735 line_editor = line_editor.with_transient_prompt(transient_prompt);
736 let input = line_editor.read_line(nu_prompt);
737 line_editor = line_editor
740 .with_highlighter(Box::<NoOpHighlighter>::default())
742 .with_completer(Box::<DefaultCompleter>::default())
744 .with_immediately_accept(false);
746
747 let shell_integration = &config.shell_integration;
748
749 let mut stack = Arc::unwrap_or_clone(stack_arc);
752
753 perf!("line_editor setup", start_time, use_color);
754
755 let line_editor_input_time = Instant::now();
756 match input {
757 Ok(Signal::Success(command)) => {
758 line_editor = run_command(RunContext {
759 engine_state,
760 stack: &mut stack,
761 line_editor,
762 command,
763 hostname,
764 use_color,
765 shell_integration,
766 entry_num,
767 });
768 }
769 Ok(Signal::HostCommand(command)) => {
770 *is_hostcommand = true;
771 line_editor = run_command(RunContext {
772 engine_state,
773 stack: &mut stack,
774 line_editor,
775 command,
776 hostname,
777 use_color,
778 shell_integration,
779 entry_num,
780 });
781 }
782 Ok(Signal::CtrlC) => {
783 run_finaliziation_ansi_sequence(
785 &stack,
786 engine_state,
787 use_color,
788 shell_integration.osc633,
789 shell_integration.osc133,
790 );
791 }
792 Ok(Signal::CtrlD) => {
793 run_finaliziation_ansi_sequence(
796 &stack,
797 engine_state,
798 use_color,
799 shell_integration.osc633,
800 shell_integration.osc133,
801 );
802
803 println!();
804
805 cleanup_exit((), engine_state, 0);
806
807 return (true, stack, line_editor);
809 }
810 Ok(_) => {}
812 Err(err) => {
813 if !err.to_string().contains("duration") {
814 write_repl_error_details(&err);
815 cleanup_exit((), engine_state, 1);
816 return (true, stack, line_editor);
817 }
818
819 run_finaliziation_ansi_sequence(
820 &stack,
821 engine_state,
822 use_color,
823 shell_integration.osc633,
824 shell_integration.osc133,
825 );
826 }
827 }
828 perf!(
829 "processing line editor input",
830 line_editor_input_time,
831 use_color
832 );
833
834 perf!(
835 "time between prompts in line editor loop",
836 loop_start_time,
837 use_color
838 );
839
840 (true, stack, line_editor)
841}
842
843fn prepare_history_metadata(
847 s: &str,
848 hostname: Option<&str>,
849 engine_state: &EngineState,
850 line_editor: &mut Reedline,
851) {
852 if !s.is_empty() && line_editor.has_last_command_context() {
853 let result = line_editor
854 .update_last_command_context(&|mut c| {
855 c.start_timestamp = Some(chrono::Utc::now());
856 c.hostname = hostname.map(str::to_string);
857 c.cwd = engine_state
858 .cwd(None)
859 .ok()
860 .map(|path| path.to_string_lossy().to_string());
861 c
862 })
863 .into_diagnostic();
864 if let Err(e) = result {
865 warn!("Could not prepare history metadata: {e}");
866 }
867 }
868}
869
870fn fill_in_result_related_history_metadata(
874 s: &str,
875 engine_state: &EngineState,
876 cmd_duration: Duration,
877 stack: &mut Stack,
878 line_editor: &mut Reedline,
879) -> Result<()> {
880 if !s.is_empty() && line_editor.has_last_command_context() {
881 line_editor
882 .update_last_command_context(&|mut c| {
883 c.duration = Some(cmd_duration);
884 c.exit_status = stack
885 .get_env_var(engine_state, "LAST_EXIT_CODE")
886 .and_then(|e| e.as_int().ok());
887 c
888 })
889 .into_diagnostic()?; }
891 Ok(())
892}
893
894enum ReplOperation {
896 AutoCd {
898 cwd: String,
900 target: PathBuf,
902 span: Span,
904 },
905 RunCommand(String),
907 DoNothing,
909}
910
911fn parse_operation(
919 s: String,
920 engine_state: &EngineState,
921 stack: &Stack,
922) -> Result<ReplOperation, ErrReport> {
923 let tokens = lex(s.as_bytes(), 0, &[], &[], false);
924 let cwd = engine_state
926 .cwd(Some(stack))
927 .map(|p| p.to_string_lossy().to_string())
928 .unwrap_or_default();
929 let mut orig = s.trim().to_string();
930 if orig.starts_with('`') {
931 orig = trim_quotes_str(&orig).to_string()
932 }
933
934 let path = nu_path::expand_path_with(&orig, &cwd, true);
935 if (engine_state.get_config().auto_cd_implicit || looks_like_path(&orig))
936 && path.is_dir()
937 && tokens.0.len() == 1
938 {
939 Ok(ReplOperation::AutoCd {
940 cwd,
941 target: path,
942 span: tokens.0[0].span,
943 })
944 } else if !s.trim().is_empty() {
945 Ok(ReplOperation::RunCommand(s))
946 } else {
947 Ok(ReplOperation::DoNothing)
948 }
949}
950
951fn do_auto_cd(
955 path: PathBuf,
956 cwd: String,
957 stack: &mut Stack,
958 engine_state: &mut EngineState,
959 span: Span,
960) {
961 let path = {
962 if !path.exists() {
963 report_shell_error(
964 Some(stack),
965 engine_state,
966 &ShellError::Io(IoError::new_with_additional_context(
967 shell_error::io::ErrorKind::DirectoryNotFound,
968 span,
969 PathBuf::from(&path),
970 "Cannot change directory",
971 )),
972 );
973 }
974 path.to_string_lossy().to_string()
975 };
976
977 if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
978 report_shell_error(
979 Some(stack),
980 engine_state,
981 &ShellError::Io(IoError::new_with_additional_context(
982 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
983 span,
984 PathBuf::from(path),
985 "Cannot change directory",
986 )),
987 );
988 return;
989 }
990
991 stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), span));
992
993 if let Err(err) = stack.set_cwd(&path) {
996 report_shell_error(Some(stack), engine_state, &err);
997 return;
998 };
999 let cwd = Value::string(cwd, span);
1000
1001 let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
1002 let mut shells = if let Some(v) = shells {
1003 v.clone().into_list().unwrap_or_else(|_| vec![cwd])
1004 } else {
1005 vec![cwd]
1006 };
1007
1008 let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
1009 let current_shell = if let Some(v) = current_shell {
1010 v.as_int().unwrap_or_default() as usize
1011 } else {
1012 0
1013 };
1014
1015 let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
1016 let last_shell = if let Some(v) = last_shell {
1017 v.as_int().unwrap_or_default() as usize
1018 } else {
1019 0
1020 };
1021
1022 shells[current_shell] = Value::string(path, span);
1023
1024 stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
1025 stack.add_env_var(
1026 "NUSHELL_LAST_SHELL".into(),
1027 Value::int(last_shell as i64, span),
1028 );
1029 stack.set_last_exit_code(0, span);
1030}
1031
1032fn do_run_cmd(
1037 s: &str,
1038 stack: &mut Stack,
1039 engine_state: &mut EngineState,
1040 line_editor: Reedline,
1043 shell_integration_osc2: bool,
1044 entry_num: usize,
1045 use_color: bool,
1046) -> Reedline {
1047 trace!("eval source: {s}");
1048
1049 let mut cmds = s.split_whitespace();
1050
1051 let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
1052
1053 if let Some("exit") = cmds.next() {
1054 let mut working_set = StateWorkingSet::new(engine_state);
1055 let _ = parse(&mut working_set, None, s.as_bytes(), false);
1056
1057 if working_set.parse_errors.is_empty() {
1058 match cmds.next() {
1059 Some(s) => {
1060 if let Ok(n) = s.parse::<i32>() {
1061 return cleanup_exit(line_editor, engine_state, n);
1062 }
1063 }
1064 None => {
1065 return cleanup_exit(line_editor, engine_state, 0);
1066 }
1067 }
1068 }
1069 }
1070
1071 if shell_integration_osc2 {
1072 run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
1073 }
1074
1075 eval_source(
1076 engine_state,
1077 stack,
1078 s.as_bytes(),
1079 &format!("repl_entry #{entry_num}"),
1080 PipelineData::empty(),
1081 false,
1082 );
1083
1084 if had_warning_before && engine_state.is_interactive {
1087 engine_state
1088 .exit_warning_given
1089 .store(false, Ordering::SeqCst);
1090 }
1091
1092 line_editor
1093}
1094
1095fn run_shell_integration_osc2(
1101 command_name: Option<&str>,
1102 engine_state: &EngineState,
1103 stack: &mut Stack,
1104 use_color: bool,
1105) {
1106 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1107 let start_time = Instant::now();
1108
1109 let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1111 let home_dir_str = p.as_path().display().to_string();
1112 if path.starts_with(&home_dir_str) {
1113 path.replacen(&home_dir_str, "~", 1)
1114 } else {
1115 path
1116 }
1117 } else {
1118 path
1119 };
1120
1121 let title = match command_name {
1122 Some(binary_name) => {
1123 let split_binary_name = binary_name.split_whitespace().next();
1124 if let Some(binary_name) = split_binary_name {
1125 format!("{maybe_abbrev_path}> {binary_name}")
1126 } else {
1127 maybe_abbrev_path.to_string()
1128 }
1129 }
1130 None => maybe_abbrev_path.to_string(),
1131 };
1132
1133 run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1139
1140 perf!("set title with command osc2", start_time, use_color);
1141 }
1142}
1143
1144fn run_shell_integration_osc7(
1145 hostname: Option<&str>,
1146 engine_state: &EngineState,
1147 stack: &mut Stack,
1148 use_color: bool,
1149) {
1150 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1151 let start_time = Instant::now();
1152
1153 let path = if cfg!(windows) {
1154 path.replace('\\', "/")
1155 } else {
1156 path
1157 };
1158
1159 run_ansi_sequence(&format!(
1161 "\x1b]7;file://{}{}{}\x1b\\",
1162 percent_encoding::utf8_percent_encode(
1163 hostname.unwrap_or("localhost"),
1164 percent_encoding::CONTROLS
1165 ),
1166 if path.starts_with('/') { "" } else { "/" },
1167 percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1168 ));
1169
1170 perf!(
1171 "communicate path to terminal with osc7",
1172 start_time,
1173 use_color
1174 );
1175 }
1176}
1177
1178fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1179 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1180 let start_time = Instant::now();
1181
1182 run_ansi_sequence(&format!("\x1b]9;9;{}\x1b\\", path));
1185
1186 perf!(
1187 "communicate path to terminal with osc9;9",
1188 start_time,
1189 use_color
1190 );
1191 }
1192}
1193
1194fn run_shell_integration_osc633(
1195 engine_state: &EngineState,
1196 stack: &mut Stack,
1197 use_color: bool,
1198 repl_cmd_line_text: String,
1199) {
1200 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1201 if stack
1204 .get_env_var(engine_state, "TERM_PROGRAM")
1205 .and_then(|v| v.as_str().ok())
1206 == Some("vscode")
1207 {
1208 let start_time = Instant::now();
1209
1210 run_ansi_sequence(&format!(
1213 "{VSCODE_CWD_PROPERTY_MARKER_PREFIX}{path}{VSCODE_CWD_PROPERTY_MARKER_SUFFIX}"
1214 ));
1215
1216 perf!(
1217 "communicate path to terminal with osc633;P",
1218 start_time,
1219 use_color
1220 );
1221
1222 let replaced_cmd_text =
1225 escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1226
1227 run_ansi_sequence(&format!(
1229 "{VSCODE_COMMANDLINE_MARKER_PREFIX}{replaced_cmd_text}{VSCODE_COMMANDLINE_MARKER_SUFFIX}"
1230 ));
1231 }
1232 }
1233}
1234
1235fn run_shell_integration_reset_application_mode() {
1236 run_ansi_sequence(RESET_APPLICATION_MODE);
1237}
1238
1239fn flush_engine_state_repl_buffer(
1243 engine_state: &mut EngineState,
1244 mut line_editor: Reedline,
1245) -> Reedline {
1246 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1247 line_editor.run_edit_commands(&[
1248 EditCommand::Clear,
1249 EditCommand::InsertString(repl.buffer.to_string()),
1250 EditCommand::MoveToPosition {
1251 position: repl.cursor_pos,
1252 select: false,
1253 },
1254 ]);
1255 if repl.accept {
1256 line_editor = line_editor.with_immediately_accept(true)
1257 }
1258 repl.accept = false;
1259 repl.buffer = "".to_string();
1260 repl.cursor_pos = 0;
1261 line_editor
1262}
1263
1264fn setup_history(
1268 engine_state: &mut EngineState,
1269 line_editor: Reedline,
1270 history: HistoryConfig,
1271) -> Result<Reedline> {
1272 let history_session_id = if history.isolation {
1274 Reedline::create_history_session_id()
1275 } else {
1276 None
1277 };
1278
1279 if let Some(path) = history.file_path() {
1280 return update_line_editor_history(
1281 engine_state,
1282 path,
1283 history,
1284 line_editor,
1285 history_session_id,
1286 );
1287 };
1288 Ok(line_editor)
1289}
1290
1291fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1295 match create_keybindings(engine_state.get_config()) {
1296 Ok(keybindings) => match keybindings {
1297 KeybindingsMode::Emacs(keybindings) => {
1298 let edit_mode = Box::new(Emacs::new(keybindings));
1299 line_editor.with_edit_mode(edit_mode)
1300 }
1301 KeybindingsMode::Vi {
1302 insert_keybindings,
1303 normal_keybindings,
1304 } => {
1305 let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1306 line_editor.with_edit_mode(edit_mode)
1307 }
1308 },
1309 Err(e) => {
1310 report_shell_error(None, engine_state, &e);
1311 line_editor
1312 }
1313 }
1314}
1315
1316fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1320 if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1321 warn!("Terminal doesn't support use_kitty_protocol config");
1322 }
1323}
1324
1325fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1326 let session_id = line_editor
1327 .get_history_session_id()
1328 .map(i64::from)
1329 .unwrap_or(0);
1330
1331 engine_state.history_session_id = session_id;
1332}
1333
1334fn update_line_editor_history(
1335 engine_state: &mut EngineState,
1336 history_path: PathBuf,
1337 history: HistoryConfig,
1338 line_editor: Reedline,
1339 history_session_id: Option<HistorySessionId>,
1340) -> Result<Reedline, ErrReport> {
1341 let ignore_space_prefixed = history.ignore_space_prefixed;
1342 let history: Box<dyn reedline::History> = match history.file_format {
1343 HistoryFileFormat::Plaintext => Box::new(
1344 FileBackedHistory::with_file(history.max_size as usize, history_path)
1345 .into_diagnostic()?,
1346 ),
1347 #[cfg(not(feature = "sqlite"))]
1349 HistoryFileFormat::Sqlite => {
1350 return Err(miette::miette!(
1351 help = "compile Nushell with the `sqlite` feature to use this",
1352 "Unsupported history file format",
1353 ));
1354 }
1355 #[cfg(feature = "sqlite")]
1356 HistoryFileFormat::Sqlite => Box::new(
1357 SqliteBackedHistory::with_file(
1358 history_path.to_path_buf(),
1359 history_session_id,
1360 Some(chrono::Utc::now()),
1361 )
1362 .into_diagnostic()?,
1363 ),
1364 };
1365 let line_editor = line_editor
1366 .with_history_session_id(history_session_id)
1367 .with_history_exclusion_prefix(ignore_space_prefixed.then_some(" ".into()))
1368 .with_history(history);
1369
1370 store_history_id_in_engine(engine_state, &line_editor);
1371
1372 Ok(line_editor)
1373}
1374
1375fn confirm_stdin_is_terminal() -> Result<()> {
1376 if !std::io::stdin().is_terminal() {
1379 return Err(std::io::Error::new(
1380 std::io::ErrorKind::NotFound,
1381 "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1382 ))
1383 .into_diagnostic();
1384 }
1385 Ok(())
1386}
1387fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1388 match shape {
1389 NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1390 NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1391 NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1392 NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1393 NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1394 NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1395 NuCursorShape::Inherit => None,
1396 }
1397}
1398
1399fn get_command_finished_marker(
1400 stack: &Stack,
1401 engine_state: &EngineState,
1402 shell_integration_osc633: bool,
1403 shell_integration_osc133: bool,
1404) -> String {
1405 let exit_code = stack
1406 .get_env_var(engine_state, "LAST_EXIT_CODE")
1407 .and_then(|e| e.as_int().ok());
1408
1409 if shell_integration_osc633 {
1410 if stack
1411 .get_env_var(engine_state, "TERM_PROGRAM")
1412 .and_then(|v| v.as_str().ok())
1413 == Some("vscode")
1414 {
1415 format!(
1417 "{}{}{}",
1418 VSCODE_POST_EXECUTION_MARKER_PREFIX,
1419 exit_code.unwrap_or(0),
1420 VSCODE_POST_EXECUTION_MARKER_SUFFIX
1421 )
1422 } else if shell_integration_osc133 {
1423 format!(
1425 "{}{}{}",
1426 POST_EXECUTION_MARKER_PREFIX,
1427 exit_code.unwrap_or(0),
1428 POST_EXECUTION_MARKER_SUFFIX
1429 )
1430 } else {
1431 "\x1b[0m".to_string()
1433 }
1434 } else if shell_integration_osc133 {
1435 format!(
1436 "{}{}{}",
1437 POST_EXECUTION_MARKER_PREFIX,
1438 exit_code.unwrap_or(0),
1439 POST_EXECUTION_MARKER_SUFFIX
1440 )
1441 } else {
1442 "\x1b[0m".to_string()
1443 }
1444}
1445
1446fn run_ansi_sequence(seq: &str) {
1447 if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1448 warn!("Error writing ansi sequence {e}");
1449 } else if let Err(e) = io::stdout().flush() {
1450 warn!("Error flushing stdio {e}");
1451 }
1452}
1453
1454fn write_repl_error_details(error: &impl std::fmt::Debug) {
1455 let _ = stderr_write_all_and_flush(format!("Error: {error:?}\n"));
1456}
1457
1458fn run_finaliziation_ansi_sequence(
1459 stack: &Stack,
1460 engine_state: &EngineState,
1461 use_color: bool,
1462 shell_integration_osc633: bool,
1463 shell_integration_osc133: bool,
1464) {
1465 if shell_integration_osc633 {
1466 if stack
1468 .get_env_var(engine_state, "TERM_PROGRAM")
1469 .and_then(|v| v.as_str().ok())
1470 == Some("vscode")
1471 {
1472 let start_time = Instant::now();
1473
1474 run_ansi_sequence(&get_command_finished_marker(
1475 stack,
1476 engine_state,
1477 shell_integration_osc633,
1478 shell_integration_osc133,
1479 ));
1480
1481 perf!(
1482 "post_execute_marker (633;D) ansi escape sequences",
1483 start_time,
1484 use_color
1485 );
1486 } else if shell_integration_osc133 {
1487 let start_time = Instant::now();
1488
1489 run_ansi_sequence(&get_command_finished_marker(
1490 stack,
1491 engine_state,
1492 shell_integration_osc633,
1493 shell_integration_osc133,
1494 ));
1495
1496 perf!(
1497 "post_execute_marker (133;D) ansi escape sequences",
1498 start_time,
1499 use_color
1500 );
1501 }
1502 } else if shell_integration_osc133 {
1503 let start_time = Instant::now();
1504
1505 run_ansi_sequence(&get_command_finished_marker(
1506 stack,
1507 engine_state,
1508 shell_integration_osc633,
1509 shell_integration_osc133,
1510 ));
1511
1512 perf!(
1513 "post_execute_marker (133;D) ansi escape sequences",
1514 start_time,
1515 use_color
1516 );
1517 }
1518}
1519
1520#[cfg(windows)]
1522static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1523 fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1524});
1525
1526fn looks_like_path(orig: &str) -> bool {
1528 #[cfg(windows)]
1529 {
1530 if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1531 return true;
1532 }
1533 }
1534
1535 orig.starts_with('.')
1536 || orig.starts_with('~')
1537 || orig.starts_with('/')
1538 || orig.starts_with('\\')
1539 || orig.ends_with(std::path::MAIN_SEPARATOR)
1540}
1541
1542#[cfg(test)]
1543mod semantic_marker_tests {
1544 use super::semantic_markers_from_config;
1545 use nu_protocol::Config;
1546 use reedline::PromptKind;
1547
1548 #[test]
1549 fn semantic_markers_use_osc633_in_vscode() {
1550 let mut config = Config::default();
1551 config.shell_integration.osc633 = true;
1552 config.shell_integration.osc133 = true;
1553
1554 let markers =
1555 semantic_markers_from_config(&config, true).expect("expected semantic markers");
1556
1557 assert_eq!(
1558 markers.prompt_start(PromptKind::Primary).as_ref(),
1559 "\x1b]633;A;k=i\x1b\\"
1560 );
1561 }
1562
1563 #[test]
1564 fn semantic_markers_use_osc133_click_events() {
1565 let mut config = Config::default();
1566 config.shell_integration.osc133 = true;
1567
1568 let markers =
1569 semantic_markers_from_config(&config, false).expect("expected semantic markers");
1570
1571 assert_eq!(
1572 markers.prompt_start(PromptKind::Primary).as_ref(),
1573 "\x1b]133;A;k=i;click_events=1\x1b\\"
1574 );
1575 }
1576
1577 #[test]
1578 fn semantic_markers_none_when_disabled() {
1579 let mut config = Config::default();
1580 config.shell_integration.osc133 = false;
1581 config.shell_integration.osc633 = false;
1582 assert!(semantic_markers_from_config(&config, false).is_none());
1583 }
1584}
1585
1586#[cfg(windows)]
1587#[test]
1588fn looks_like_path_windows_drive_path_works() {
1589 assert!(looks_like_path("C:"));
1590 assert!(looks_like_path("D:\\"));
1591 assert!(looks_like_path("E:/"));
1592 assert!(looks_like_path("F:\\some_dir"));
1593 assert!(looks_like_path("G:/some_dir"));
1594}
1595
1596#[cfg(windows)]
1597#[test]
1598fn trailing_slash_looks_like_path() {
1599 assert!(looks_like_path("foo\\"))
1600}
1601
1602#[cfg(not(windows))]
1603#[test]
1604fn trailing_slash_looks_like_path() {
1605 assert!(looks_like_path("foo/"))
1606}
1607
1608#[test]
1609fn are_session_ids_in_sync() {
1610 let engine_state = &mut EngineState::new();
1611 let history = engine_state.history_config().unwrap();
1612 let history_path = history.file_path().unwrap();
1613 let line_editor = reedline::Reedline::create();
1614 let history_session_id = reedline::Reedline::create_history_session_id();
1615 let line_editor = update_line_editor_history(
1616 engine_state,
1617 history_path,
1618 history,
1619 line_editor,
1620 history_session_id,
1621 );
1622 assert_eq!(
1623 i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1624 engine_state.history_session_id
1625 );
1626}
1627
1628#[cfg(test)]
1629mod test_auto_cd {
1630 use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1631 use nu_path::AbsolutePath;
1632 use nu_protocol::engine::{EngineState, Stack};
1633 use tempfile::tempdir;
1634
1635 #[cfg(any(unix, windows))]
1637 fn symlink(
1638 original: impl AsRef<AbsolutePath>,
1639 link: impl AsRef<AbsolutePath>,
1640 ) -> std::io::Result<()> {
1641 let original = original.as_ref();
1642 let link = link.as_ref();
1643
1644 #[cfg(unix)]
1645 {
1646 std::os::unix::fs::symlink(original, link)
1647 }
1648 #[cfg(windows)]
1649 {
1650 if original.is_dir() {
1651 std::os::windows::fs::symlink_dir(original, link)
1652 } else {
1653 std::os::windows::fs::symlink_file(original, link)
1654 }
1655 }
1656 }
1657
1658 #[track_caller]
1662 fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1663 let mut engine_state = EngineState::new();
1665 let mut stack = Stack::new();
1666 stack.set_cwd(before.as_ref()).unwrap();
1667
1668 let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1670 let ReplOperation::AutoCd { cwd, target, span } = op else {
1671 panic!("'{input}' was not parsed into an auto-cd operation")
1672 };
1673
1674 do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1676 let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1677
1678 let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1682 let after = std::fs::canonicalize(after.as_ref()).unwrap();
1683 assert_eq!(updated_cwd, after);
1684 }
1685
1686 #[test]
1687 fn auto_cd_root() {
1688 let tempdir = tempdir().unwrap();
1689 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1690
1691 let input = if cfg!(windows) { r"C:\" } else { "/" };
1692 let root = AbsolutePath::try_new(input).unwrap();
1693 check(tempdir, input, root);
1694 }
1695
1696 #[test]
1697 fn auto_cd_tilde() {
1698 let tempdir = tempdir().unwrap();
1699 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1700
1701 let home = nu_path::home_dir().unwrap();
1702 check(tempdir, "~", home);
1703 }
1704
1705 #[test]
1706 fn auto_cd_dot() {
1707 let tempdir = tempdir().unwrap();
1708 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1709
1710 check(tempdir, ".", tempdir);
1711 }
1712
1713 #[test]
1714 fn auto_cd_double_dot() {
1715 let tempdir = tempdir().unwrap();
1716 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1717
1718 let dir = tempdir.join("foo");
1719 std::fs::create_dir_all(&dir).unwrap();
1720 check(dir, "..", tempdir);
1721 }
1722
1723 #[test]
1724 fn auto_cd_triple_dot() {
1725 let tempdir = tempdir().unwrap();
1726 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1727
1728 let dir = tempdir.join("foo").join("bar");
1729 std::fs::create_dir_all(&dir).unwrap();
1730 check(dir, "...", tempdir);
1731 }
1732
1733 #[test]
1734 fn auto_cd_relative() {
1735 let tempdir = tempdir().unwrap();
1736 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1737
1738 let foo = tempdir.join("foo");
1739 let bar = tempdir.join("bar");
1740 std::fs::create_dir_all(&foo).unwrap();
1741 std::fs::create_dir_all(&bar).unwrap();
1742 let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1743 check(foo, input, bar);
1744 }
1745
1746 #[test]
1747 fn auto_cd_trailing_slash() {
1748 let tempdir = tempdir().unwrap();
1749 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1750
1751 let dir = tempdir.join("foo");
1752 std::fs::create_dir_all(&dir).unwrap();
1753 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1754 check(tempdir, input, dir);
1755 }
1756
1757 #[test]
1758 fn auto_cd_symlink() {
1759 let tempdir = tempdir().unwrap();
1760 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1761
1762 let dir = tempdir.join("foo");
1763 std::fs::create_dir_all(&dir).unwrap();
1764 let link = tempdir.join("link");
1765 symlink(&dir, &link).unwrap();
1766 let input = if cfg!(windows) { r".\link" } else { "./link" };
1767 check(tempdir, input, link);
1768
1769 let dir = tempdir.join("foo").join("bar");
1770 std::fs::create_dir_all(&dir).unwrap();
1771 let link = tempdir.join("link2");
1772 symlink(&dir, &link).unwrap();
1773 let input = "..";
1774 check(link, input, tempdir);
1775 }
1776
1777 #[test]
1778 #[should_panic(expected = "was not parsed into an auto-cd operation")]
1779 fn auto_cd_nonexistent_directory() {
1780 let tempdir = tempdir().unwrap();
1781 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1782
1783 let dir = tempdir.join("foo");
1784 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1785 check(tempdir, input, dir);
1786 }
1787
1788 #[test]
1789 fn escape_vscode_semicolon_test() {
1790 let input = "now;is";
1791 let expected = r#"now\x3Bis"#;
1792 let actual = escape_special_vscode_bytes(input).unwrap();
1793 assert_eq!(expected, actual);
1794 }
1795
1796 #[test]
1797 fn escape_vscode_backslash_test() {
1798 let input = r#"now\is"#;
1799 let expected = r#"now\\is"#;
1800 let actual = escape_special_vscode_bytes(input).unwrap();
1801 assert_eq!(expected, actual);
1802 }
1803
1804 #[test]
1805 fn escape_vscode_linefeed_test() {
1806 let input = "now\nis";
1807 let expected = r#"now\x0Ais"#;
1808 let actual = escape_special_vscode_bytes(input).unwrap();
1809 assert_eq!(expected, actual);
1810 }
1811
1812 #[test]
1813 fn escape_vscode_tab_null_cr_test() {
1814 let input = "now\t\0\ris";
1815 let expected = r#"now\x09\x00\x0Dis"#;
1816 let actual = escape_special_vscode_bytes(input).unwrap();
1817 assert_eq!(expected, actual);
1818 }
1819
1820 #[test]
1821 fn escape_vscode_multibyte_ok() {
1822 let input = "now🍪is";
1823 let actual = escape_special_vscode_bytes(input).unwrap();
1824 assert_eq!(input, actual);
1825 }
1826}