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