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