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