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