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;
26use nu_protocol::shell_error::io::IoError;
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 Value::Bool { val: false, .. } => {}
149 Value::String { ref val, .. } if val == "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 _ => {
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{:02X}", byte).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(err) = hook::eval_hooks(
330 engine_state,
331 &mut stack,
332 vec![],
333 &engine_state.get_config().hooks.pre_prompt.clone(),
334 "pre_prompt",
335 ) {
336 report_shell_error(engine_state, &err);
337 }
338 perf!("pre-prompt hook", 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(engine_state, &error)
349 }
350 perf!("env-change 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
496 let shell_integration_osc2 = config.shell_integration.osc2;
498 let shell_integration_osc7 = config.shell_integration.osc7;
499 let shell_integration_osc9_9 = config.shell_integration.osc9_9;
500 let shell_integration_osc133 = config.shell_integration.osc133;
501 let shell_integration_osc633 = config.shell_integration.osc633;
502 let shell_integration_reset_application_mode = config.shell_integration.reset_application_mode;
503
504 let mut stack = Arc::unwrap_or_clone(stack_arc);
507
508 perf!("line_editor setup", start_time, use_color);
509
510 let line_editor_input_time = std::time::Instant::now();
511 match input {
512 Ok(Signal::Success(repl_cmd_line_text)) => {
513 let history_supports_meta = matches!(
514 engine_state.history_config().map(|h| h.file_format),
515 Some(HistoryFileFormat::Sqlite)
516 );
517
518 if history_supports_meta {
519 prepare_history_metadata(
520 &repl_cmd_line_text,
521 hostname,
522 engine_state,
523 &mut line_editor,
524 );
525 }
526
527 start_time = Instant::now();
529
530 {
533 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
535 repl.buffer = repl_cmd_line_text.to_string();
536 drop(repl);
537
538 if let Err(err) = hook::eval_hooks(
539 engine_state,
540 &mut stack,
541 vec![],
542 &engine_state.get_config().hooks.pre_execution.clone(),
543 "pre_execution",
544 ) {
545 report_shell_error(engine_state, &err);
546 }
547 }
548
549 perf!("pre_execution_hook", start_time, use_color);
550
551 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
552 repl.cursor_pos = line_editor.current_insertion_point();
553 repl.buffer = line_editor.current_buffer_contents().to_string();
554 drop(repl);
555
556 if shell_integration_osc633 {
557 if stack
558 .get_env_var(engine_state, "TERM_PROGRAM")
559 .and_then(|v| v.as_str().ok())
560 == Some("vscode")
561 {
562 start_time = Instant::now();
563
564 run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
565
566 perf!(
567 "pre_execute_marker (633;C) ansi escape sequence",
568 start_time,
569 use_color
570 );
571 } else if shell_integration_osc133 {
572 start_time = Instant::now();
573
574 run_ansi_sequence(PRE_EXECUTION_MARKER);
575
576 perf!(
577 "pre_execute_marker (133;C) ansi escape sequence",
578 start_time,
579 use_color
580 );
581 }
582 } else if shell_integration_osc133 {
583 start_time = Instant::now();
584
585 run_ansi_sequence(PRE_EXECUTION_MARKER);
586
587 perf!(
588 "pre_execute_marker (133;C) ansi escape sequence",
589 start_time,
590 use_color
591 );
592 }
593
594 let cmd_execution_start_time = Instant::now();
596
597 match parse_operation(repl_cmd_line_text.clone(), engine_state, &stack) {
598 Ok(operation) => match operation {
599 ReplOperation::AutoCd { cwd, target, span } => {
600 do_auto_cd(target, cwd, &mut stack, engine_state, span);
601
602 run_finaliziation_ansi_sequence(
603 &stack,
604 engine_state,
605 use_color,
606 shell_integration_osc633,
607 shell_integration_osc133,
608 );
609 }
610 ReplOperation::RunCommand(cmd) => {
611 line_editor = do_run_cmd(
612 &cmd,
613 &mut stack,
614 engine_state,
615 line_editor,
616 shell_integration_osc2,
617 *entry_num,
618 use_color,
619 );
620
621 run_finaliziation_ansi_sequence(
622 &stack,
623 engine_state,
624 use_color,
625 shell_integration_osc633,
626 shell_integration_osc133,
627 );
628 }
629 ReplOperation::DoNothing => {}
631 },
632 Err(ref e) => error!("Error parsing operation: {e}"),
633 }
634 let cmd_duration = cmd_execution_start_time.elapsed();
635
636 stack.add_env_var(
637 "CMD_DURATION_MS".into(),
638 Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
639 );
640
641 if history_supports_meta {
642 if let Err(e) = fill_in_result_related_history_metadata(
643 &repl_cmd_line_text,
644 engine_state,
645 cmd_duration,
646 &mut stack,
647 &mut line_editor,
648 ) {
649 warn!("Could not fill in result related history metadata: {e}");
650 }
651 }
652
653 if shell_integration_osc2 {
654 run_shell_integration_osc2(None, engine_state, &mut stack, use_color);
655 }
656 if shell_integration_osc7 {
657 run_shell_integration_osc7(hostname, engine_state, &mut stack, use_color);
658 }
659 if shell_integration_osc9_9 {
660 run_shell_integration_osc9_9(engine_state, &mut stack, use_color);
661 }
662 if shell_integration_osc633 {
663 run_shell_integration_osc633(
664 engine_state,
665 &mut stack,
666 use_color,
667 repl_cmd_line_text,
668 );
669 }
670 if shell_integration_reset_application_mode {
671 run_shell_integration_reset_application_mode();
672 }
673
674 flush_engine_state_repl_buffer(engine_state, &mut line_editor);
675 }
676 Ok(Signal::CtrlC) => {
677 run_finaliziation_ansi_sequence(
679 &stack,
680 engine_state,
681 use_color,
682 shell_integration_osc633,
683 shell_integration_osc133,
684 );
685 }
686 Ok(Signal::CtrlD) => {
687 run_finaliziation_ansi_sequence(
690 &stack,
691 engine_state,
692 use_color,
693 shell_integration_osc633,
694 shell_integration_osc133,
695 );
696
697 println!();
698
699 cleanup_exit((), engine_state, 0);
700
701 return (true, stack, line_editor);
703 }
704 Err(err) => {
705 let message = err.to_string();
706 if !message.contains("duration") {
707 eprintln!("Error: {err:?}");
708 }
713
714 run_finaliziation_ansi_sequence(
715 &stack,
716 engine_state,
717 use_color,
718 shell_integration_osc633,
719 shell_integration_osc133,
720 );
721 }
722 }
723 perf!(
724 "processing line editor input",
725 line_editor_input_time,
726 use_color
727 );
728
729 perf!(
730 "time between prompts in line editor loop",
731 loop_start_time,
732 use_color
733 );
734
735 (true, stack, line_editor)
736}
737
738fn prepare_history_metadata(
742 s: &str,
743 hostname: Option<&str>,
744 engine_state: &EngineState,
745 line_editor: &mut Reedline,
746) {
747 if !s.is_empty() && line_editor.has_last_command_context() {
748 let result = line_editor
749 .update_last_command_context(&|mut c| {
750 c.start_timestamp = Some(chrono::Utc::now());
751 c.hostname = hostname.map(str::to_string);
752 c.cwd = engine_state
753 .cwd(None)
754 .ok()
755 .map(|path| path.to_string_lossy().to_string());
756 c
757 })
758 .into_diagnostic();
759 if let Err(e) = result {
760 warn!("Could not prepare history metadata: {e}");
761 }
762 }
763}
764
765fn fill_in_result_related_history_metadata(
769 s: &str,
770 engine_state: &EngineState,
771 cmd_duration: Duration,
772 stack: &mut Stack,
773 line_editor: &mut Reedline,
774) -> Result<()> {
775 if !s.is_empty() && line_editor.has_last_command_context() {
776 line_editor
777 .update_last_command_context(&|mut c| {
778 c.duration = Some(cmd_duration);
779 c.exit_status = stack
780 .get_env_var(engine_state, "LAST_EXIT_CODE")
781 .and_then(|e| e.as_int().ok());
782 c
783 })
784 .into_diagnostic()?; }
786 Ok(())
787}
788
789enum ReplOperation {
791 AutoCd {
793 cwd: String,
795 target: PathBuf,
797 span: Span,
799 },
800 RunCommand(String),
802 DoNothing,
804}
805
806fn parse_operation(
814 s: String,
815 engine_state: &EngineState,
816 stack: &Stack,
817) -> Result<ReplOperation, ErrReport> {
818 let tokens = lex(s.as_bytes(), 0, &[], &[], false);
819 let cwd = engine_state
821 .cwd(Some(stack))
822 .map(|p| p.to_string_lossy().to_string())
823 .unwrap_or_default();
824 let mut orig = s.clone();
825 if orig.starts_with('`') {
826 orig = trim_quotes_str(&orig).to_string()
827 }
828
829 let path = nu_path::expand_path_with(&orig, &cwd, true);
830 if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 {
831 Ok(ReplOperation::AutoCd {
832 cwd,
833 target: path,
834 span: tokens.0[0].span,
835 })
836 } else if !s.trim().is_empty() {
837 Ok(ReplOperation::RunCommand(s))
838 } else {
839 Ok(ReplOperation::DoNothing)
840 }
841}
842
843fn do_auto_cd(
847 path: PathBuf,
848 cwd: String,
849 stack: &mut Stack,
850 engine_state: &mut EngineState,
851 span: Span,
852) {
853 let path = {
854 if !path.exists() {
855 report_shell_error(
856 engine_state,
857 &ShellError::Io(IoError::new_with_additional_context(
858 shell_error::io::ErrorKind::DirectoryNotFound,
859 span,
860 PathBuf::from(&path),
861 "Cannot change directory",
862 )),
863 );
864 }
865 path.to_string_lossy().to_string()
866 };
867
868 if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
869 report_shell_error(
870 engine_state,
871 &ShellError::Io(IoError::new_with_additional_context(
872 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
873 span,
874 PathBuf::from(path),
875 "Cannot change directory",
876 )),
877 );
878 return;
879 }
880
881 stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown()));
882
883 if let Err(err) = stack.set_cwd(&path) {
886 report_shell_error(engine_state, &err);
887 return;
888 };
889 let cwd = Value::string(cwd, span);
890
891 let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
892 let mut shells = if let Some(v) = shells {
893 v.clone().into_list().unwrap_or_else(|_| vec![cwd])
894 } else {
895 vec![cwd]
896 };
897
898 let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
899 let current_shell = if let Some(v) = current_shell {
900 v.as_int().unwrap_or_default() as usize
901 } else {
902 0
903 };
904
905 let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
906 let last_shell = if let Some(v) = last_shell {
907 v.as_int().unwrap_or_default() as usize
908 } else {
909 0
910 };
911
912 shells[current_shell] = Value::string(path, span);
913
914 stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
915 stack.add_env_var(
916 "NUSHELL_LAST_SHELL".into(),
917 Value::int(last_shell as i64, span),
918 );
919 stack.set_last_exit_code(0, Span::unknown());
920}
921
922fn do_run_cmd(
927 s: &str,
928 stack: &mut Stack,
929 engine_state: &mut EngineState,
930 line_editor: Reedline,
933 shell_integration_osc2: bool,
934 entry_num: usize,
935 use_color: bool,
936) -> Reedline {
937 trace!("eval source: {}", s);
938
939 let mut cmds = s.split_whitespace();
940
941 let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
942
943 if let Some("exit") = cmds.next() {
944 let mut working_set = StateWorkingSet::new(engine_state);
945 let _ = parse(&mut working_set, None, s.as_bytes(), false);
946
947 if working_set.parse_errors.is_empty() {
948 match cmds.next() {
949 Some(s) => {
950 if let Ok(n) = s.parse::<i32>() {
951 return cleanup_exit(line_editor, engine_state, n);
952 }
953 }
954 None => {
955 return cleanup_exit(line_editor, engine_state, 0);
956 }
957 }
958 }
959 }
960
961 if shell_integration_osc2 {
962 run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
963 }
964
965 eval_source(
966 engine_state,
967 stack,
968 s.as_bytes(),
969 &format!("entry #{entry_num}"),
970 PipelineData::empty(),
971 false,
972 );
973
974 if had_warning_before && engine_state.is_interactive {
977 engine_state
978 .exit_warning_given
979 .store(false, Ordering::SeqCst);
980 }
981
982 line_editor
983}
984
985fn run_shell_integration_osc2(
991 command_name: Option<&str>,
992 engine_state: &EngineState,
993 stack: &mut Stack,
994 use_color: bool,
995) {
996 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
997 let start_time = Instant::now();
998
999 let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1001 let home_dir_str = p.as_path().display().to_string();
1002 if path.starts_with(&home_dir_str) {
1003 path.replacen(&home_dir_str, "~", 1)
1004 } else {
1005 path
1006 }
1007 } else {
1008 path
1009 };
1010
1011 let title = match command_name {
1012 Some(binary_name) => {
1013 let split_binary_name = binary_name.split_whitespace().next();
1014 if let Some(binary_name) = split_binary_name {
1015 format!("{maybe_abbrev_path}> {binary_name}")
1016 } else {
1017 maybe_abbrev_path.to_string()
1018 }
1019 }
1020 None => maybe_abbrev_path.to_string(),
1021 };
1022
1023 run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1029
1030 perf!("set title with command osc2", start_time, use_color);
1031 }
1032}
1033
1034fn run_shell_integration_osc7(
1035 hostname: Option<&str>,
1036 engine_state: &EngineState,
1037 stack: &mut Stack,
1038 use_color: bool,
1039) {
1040 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1041 let start_time = Instant::now();
1042
1043 run_ansi_sequence(&format!(
1045 "\x1b]7;file://{}{}{}\x1b\\",
1046 percent_encoding::utf8_percent_encode(
1047 hostname.unwrap_or("localhost"),
1048 percent_encoding::CONTROLS
1049 ),
1050 if path.starts_with('/') { "" } else { "/" },
1051 percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1052 ));
1053
1054 perf!(
1055 "communicate path to terminal with osc7",
1056 start_time,
1057 use_color
1058 );
1059 }
1060}
1061
1062fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1063 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1064 let start_time = Instant::now();
1065
1066 run_ansi_sequence(&format!(
1069 "\x1b]9;9;{}\x1b\\",
1070 percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1071 ));
1072
1073 perf!(
1074 "communicate path to terminal with osc9;9",
1075 start_time,
1076 use_color
1077 );
1078 }
1079}
1080
1081fn run_shell_integration_osc633(
1082 engine_state: &EngineState,
1083 stack: &mut Stack,
1084 use_color: bool,
1085 repl_cmd_line_text: String,
1086) {
1087 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1088 if stack
1091 .get_env_var(engine_state, "TERM_PROGRAM")
1092 .and_then(|v| v.as_str().ok())
1093 == Some("vscode")
1094 {
1095 let start_time = Instant::now();
1096
1097 run_ansi_sequence(&format!(
1100 "{}{}{}",
1101 VSCODE_CWD_PROPERTY_MARKER_PREFIX, path, VSCODE_CWD_PROPERTY_MARKER_SUFFIX
1102 ));
1103
1104 perf!(
1105 "communicate path to terminal with osc633;P",
1106 start_time,
1107 use_color
1108 );
1109
1110 let replaced_cmd_text =
1113 escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1114
1115 run_ansi_sequence(&format!(
1117 "{}{}{}",
1118 VSCODE_COMMANDLINE_MARKER_PREFIX,
1119 replaced_cmd_text,
1120 VSCODE_COMMANDLINE_MARKER_SUFFIX
1121 ));
1122 }
1123 }
1124}
1125
1126fn run_shell_integration_reset_application_mode() {
1127 run_ansi_sequence(RESET_APPLICATION_MODE);
1128}
1129
1130fn flush_engine_state_repl_buffer(engine_state: &mut EngineState, line_editor: &mut Reedline) {
1134 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1135 line_editor.run_edit_commands(&[
1136 EditCommand::Clear,
1137 EditCommand::InsertString(repl.buffer.to_string()),
1138 EditCommand::MoveToPosition {
1139 position: repl.cursor_pos,
1140 select: false,
1141 },
1142 ]);
1143 repl.buffer = "".to_string();
1144 repl.cursor_pos = 0;
1145}
1146
1147fn setup_history(
1151 engine_state: &mut EngineState,
1152 line_editor: Reedline,
1153 history: HistoryConfig,
1154) -> Result<Reedline> {
1155 let history_session_id = if history.isolation {
1157 Reedline::create_history_session_id()
1158 } else {
1159 None
1160 };
1161
1162 if let Some(path) = history.file_path() {
1163 return update_line_editor_history(
1164 engine_state,
1165 path,
1166 history,
1167 line_editor,
1168 history_session_id,
1169 );
1170 };
1171 Ok(line_editor)
1172}
1173
1174fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1178 match create_keybindings(engine_state.get_config()) {
1179 Ok(keybindings) => match keybindings {
1180 KeybindingsMode::Emacs(keybindings) => {
1181 let edit_mode = Box::new(Emacs::new(keybindings));
1182 line_editor.with_edit_mode(edit_mode)
1183 }
1184 KeybindingsMode::Vi {
1185 insert_keybindings,
1186 normal_keybindings,
1187 } => {
1188 let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1189 line_editor.with_edit_mode(edit_mode)
1190 }
1191 },
1192 Err(e) => {
1193 report_shell_error(engine_state, &e);
1194 line_editor
1195 }
1196 }
1197}
1198
1199fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1203 if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1204 warn!("Terminal doesn't support use_kitty_protocol config");
1205 }
1206}
1207
1208fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1209 let session_id = line_editor
1210 .get_history_session_id()
1211 .map(i64::from)
1212 .unwrap_or(0);
1213
1214 engine_state.history_session_id = session_id;
1215}
1216
1217fn update_line_editor_history(
1218 engine_state: &mut EngineState,
1219 history_path: PathBuf,
1220 history: HistoryConfig,
1221 line_editor: Reedline,
1222 history_session_id: Option<HistorySessionId>,
1223) -> Result<Reedline, ErrReport> {
1224 let history: Box<dyn reedline::History> = match history.file_format {
1225 HistoryFileFormat::Plaintext => Box::new(
1226 FileBackedHistory::with_file(history.max_size as usize, history_path)
1227 .into_diagnostic()?,
1228 ),
1229 HistoryFileFormat::Sqlite => Box::new(
1230 SqliteBackedHistory::with_file(
1231 history_path.to_path_buf(),
1232 history_session_id,
1233 Some(chrono::Utc::now()),
1234 )
1235 .into_diagnostic()?,
1236 ),
1237 };
1238 let line_editor = line_editor
1239 .with_history_session_id(history_session_id)
1240 .with_history_exclusion_prefix(Some(" ".into()))
1241 .with_history(history);
1242
1243 store_history_id_in_engine(engine_state, &line_editor);
1244
1245 Ok(line_editor)
1246}
1247
1248fn confirm_stdin_is_terminal() -> Result<()> {
1249 if !std::io::stdin().is_terminal() {
1252 return Err(std::io::Error::new(
1253 std::io::ErrorKind::NotFound,
1254 "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1255 ))
1256 .into_diagnostic();
1257 }
1258 Ok(())
1259}
1260fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1261 match shape {
1262 NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1263 NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1264 NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1265 NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1266 NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1267 NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1268 NuCursorShape::Inherit => None,
1269 }
1270}
1271
1272fn get_command_finished_marker(
1273 stack: &Stack,
1274 engine_state: &EngineState,
1275 shell_integration_osc633: bool,
1276 shell_integration_osc133: bool,
1277) -> String {
1278 let exit_code = stack
1279 .get_env_var(engine_state, "LAST_EXIT_CODE")
1280 .and_then(|e| e.as_int().ok());
1281
1282 if shell_integration_osc633 {
1283 if stack
1284 .get_env_var(engine_state, "TERM_PROGRAM")
1285 .and_then(|v| v.as_str().ok())
1286 == Some("vscode")
1287 {
1288 format!(
1290 "{}{}{}",
1291 VSCODE_POST_EXECUTION_MARKER_PREFIX,
1292 exit_code.unwrap_or(0),
1293 VSCODE_POST_EXECUTION_MARKER_SUFFIX
1294 )
1295 } else if shell_integration_osc133 {
1296 format!(
1298 "{}{}{}",
1299 POST_EXECUTION_MARKER_PREFIX,
1300 exit_code.unwrap_or(0),
1301 POST_EXECUTION_MARKER_SUFFIX
1302 )
1303 } else {
1304 "\x1b[0m".to_string()
1306 }
1307 } else if shell_integration_osc133 {
1308 format!(
1309 "{}{}{}",
1310 POST_EXECUTION_MARKER_PREFIX,
1311 exit_code.unwrap_or(0),
1312 POST_EXECUTION_MARKER_SUFFIX
1313 )
1314 } else {
1315 "\x1b[0m".to_string()
1316 }
1317}
1318
1319fn run_ansi_sequence(seq: &str) {
1320 if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1321 warn!("Error writing ansi sequence {e}");
1322 } else if let Err(e) = io::stdout().flush() {
1323 warn!("Error flushing stdio {e}");
1324 }
1325}
1326
1327fn run_finaliziation_ansi_sequence(
1328 stack: &Stack,
1329 engine_state: &EngineState,
1330 use_color: bool,
1331 shell_integration_osc633: bool,
1332 shell_integration_osc133: bool,
1333) {
1334 if shell_integration_osc633 {
1335 if stack
1337 .get_env_var(engine_state, "TERM_PROGRAM")
1338 .and_then(|v| v.as_str().ok())
1339 == Some("vscode")
1340 {
1341 let start_time = Instant::now();
1342
1343 run_ansi_sequence(&get_command_finished_marker(
1344 stack,
1345 engine_state,
1346 shell_integration_osc633,
1347 shell_integration_osc133,
1348 ));
1349
1350 perf!(
1351 "post_execute_marker (633;D) ansi escape sequences",
1352 start_time,
1353 use_color
1354 );
1355 } else if shell_integration_osc133 {
1356 let start_time = Instant::now();
1357
1358 run_ansi_sequence(&get_command_finished_marker(
1359 stack,
1360 engine_state,
1361 shell_integration_osc633,
1362 shell_integration_osc133,
1363 ));
1364
1365 perf!(
1366 "post_execute_marker (133;D) ansi escape sequences",
1367 start_time,
1368 use_color
1369 );
1370 }
1371 } else if shell_integration_osc133 {
1372 let start_time = Instant::now();
1373
1374 run_ansi_sequence(&get_command_finished_marker(
1375 stack,
1376 engine_state,
1377 shell_integration_osc633,
1378 shell_integration_osc133,
1379 ));
1380
1381 perf!(
1382 "post_execute_marker (133;D) ansi escape sequences",
1383 start_time,
1384 use_color
1385 );
1386 }
1387}
1388
1389#[cfg(windows)]
1391static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1392 fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1393});
1394
1395fn looks_like_path(orig: &str) -> bool {
1397 #[cfg(windows)]
1398 {
1399 if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1400 return true;
1401 }
1402 }
1403
1404 orig.starts_with('.')
1405 || orig.starts_with('~')
1406 || orig.starts_with('/')
1407 || orig.starts_with('\\')
1408 || orig.ends_with(std::path::MAIN_SEPARATOR)
1409}
1410
1411#[cfg(windows)]
1412#[test]
1413fn looks_like_path_windows_drive_path_works() {
1414 assert!(looks_like_path("C:"));
1415 assert!(looks_like_path("D:\\"));
1416 assert!(looks_like_path("E:/"));
1417 assert!(looks_like_path("F:\\some_dir"));
1418 assert!(looks_like_path("G:/some_dir"));
1419}
1420
1421#[cfg(windows)]
1422#[test]
1423fn trailing_slash_looks_like_path() {
1424 assert!(looks_like_path("foo\\"))
1425}
1426
1427#[cfg(not(windows))]
1428#[test]
1429fn trailing_slash_looks_like_path() {
1430 assert!(looks_like_path("foo/"))
1431}
1432
1433#[test]
1434fn are_session_ids_in_sync() {
1435 let engine_state = &mut EngineState::new();
1436 let history = engine_state.history_config().unwrap();
1437 let history_path = history.file_path().unwrap();
1438 let line_editor = reedline::Reedline::create();
1439 let history_session_id = reedline::Reedline::create_history_session_id();
1440 let line_editor = update_line_editor_history(
1441 engine_state,
1442 history_path,
1443 history,
1444 line_editor,
1445 history_session_id,
1446 );
1447 assert_eq!(
1448 i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1449 engine_state.history_session_id
1450 );
1451}
1452
1453#[cfg(test)]
1454mod test_auto_cd {
1455 use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1456 use nu_path::AbsolutePath;
1457 use nu_protocol::engine::{EngineState, Stack};
1458 use tempfile::tempdir;
1459
1460 #[cfg(any(unix, windows))]
1462 fn symlink(
1463 original: impl AsRef<AbsolutePath>,
1464 link: impl AsRef<AbsolutePath>,
1465 ) -> std::io::Result<()> {
1466 let original = original.as_ref();
1467 let link = link.as_ref();
1468
1469 #[cfg(unix)]
1470 {
1471 std::os::unix::fs::symlink(original, link)
1472 }
1473 #[cfg(windows)]
1474 {
1475 if original.is_dir() {
1476 std::os::windows::fs::symlink_dir(original, link)
1477 } else {
1478 std::os::windows::fs::symlink_file(original, link)
1479 }
1480 }
1481 }
1482
1483 #[track_caller]
1487 fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1488 let mut engine_state = EngineState::new();
1490 let mut stack = Stack::new();
1491 stack.set_cwd(before.as_ref()).unwrap();
1492
1493 let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1495 let ReplOperation::AutoCd { cwd, target, span } = op else {
1496 panic!("'{}' was not parsed into an auto-cd operation", input)
1497 };
1498
1499 do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1501 let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1502
1503 let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1507 let after = std::fs::canonicalize(after.as_ref()).unwrap();
1508 assert_eq!(updated_cwd, after);
1509 }
1510
1511 #[test]
1512 fn auto_cd_root() {
1513 let tempdir = tempdir().unwrap();
1514 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1515
1516 let input = if cfg!(windows) { r"C:\" } else { "/" };
1517 let root = AbsolutePath::try_new(input).unwrap();
1518 check(tempdir, input, root);
1519 }
1520
1521 #[test]
1522 fn auto_cd_tilde() {
1523 let tempdir = tempdir().unwrap();
1524 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1525
1526 let home = nu_path::home_dir().unwrap();
1527 check(tempdir, "~", home);
1528 }
1529
1530 #[test]
1531 fn auto_cd_dot() {
1532 let tempdir = tempdir().unwrap();
1533 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1534
1535 check(tempdir, ".", tempdir);
1536 }
1537
1538 #[test]
1539 fn auto_cd_double_dot() {
1540 let tempdir = tempdir().unwrap();
1541 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1542
1543 let dir = tempdir.join("foo");
1544 std::fs::create_dir_all(&dir).unwrap();
1545 check(dir, "..", tempdir);
1546 }
1547
1548 #[test]
1549 fn auto_cd_triple_dot() {
1550 let tempdir = tempdir().unwrap();
1551 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1552
1553 let dir = tempdir.join("foo").join("bar");
1554 std::fs::create_dir_all(&dir).unwrap();
1555 check(dir, "...", tempdir);
1556 }
1557
1558 #[test]
1559 fn auto_cd_relative() {
1560 let tempdir = tempdir().unwrap();
1561 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1562
1563 let foo = tempdir.join("foo");
1564 let bar = tempdir.join("bar");
1565 std::fs::create_dir_all(&foo).unwrap();
1566 std::fs::create_dir_all(&bar).unwrap();
1567 let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1568 check(foo, input, bar);
1569 }
1570
1571 #[test]
1572 fn auto_cd_trailing_slash() {
1573 let tempdir = tempdir().unwrap();
1574 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1575
1576 let dir = tempdir.join("foo");
1577 std::fs::create_dir_all(&dir).unwrap();
1578 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1579 check(tempdir, input, dir);
1580 }
1581
1582 #[test]
1583 fn auto_cd_symlink() {
1584 let tempdir = tempdir().unwrap();
1585 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1586
1587 let dir = tempdir.join("foo");
1588 std::fs::create_dir_all(&dir).unwrap();
1589 let link = tempdir.join("link");
1590 symlink(&dir, &link).unwrap();
1591 let input = if cfg!(windows) { r".\link" } else { "./link" };
1592 check(tempdir, input, link);
1593
1594 let dir = tempdir.join("foo").join("bar");
1595 std::fs::create_dir_all(&dir).unwrap();
1596 let link = tempdir.join("link2");
1597 symlink(&dir, &link).unwrap();
1598 let input = "..";
1599 check(link, input, tempdir);
1600 }
1601
1602 #[test]
1603 #[should_panic(expected = "was not parsed into an auto-cd operation")]
1604 fn auto_cd_nonexistent_directory() {
1605 let tempdir = tempdir().unwrap();
1606 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1607
1608 let dir = tempdir.join("foo");
1609 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1610 check(tempdir, input, dir);
1611 }
1612
1613 #[test]
1614 fn escape_vscode_semicolon_test() {
1615 let input = r#"now;is"#;
1616 let expected = r#"now\x3Bis"#;
1617 let actual = escape_special_vscode_bytes(input).unwrap();
1618 assert_eq!(expected, actual);
1619 }
1620
1621 #[test]
1622 fn escape_vscode_backslash_test() {
1623 let input = r#"now\is"#;
1624 let expected = r#"now\\is"#;
1625 let actual = escape_special_vscode_bytes(input).unwrap();
1626 assert_eq!(expected, actual);
1627 }
1628
1629 #[test]
1630 fn escape_vscode_linefeed_test() {
1631 let input = "now\nis";
1632 let expected = r#"now\x0Ais"#;
1633 let actual = escape_special_vscode_bytes(input).unwrap();
1634 assert_eq!(expected, actual);
1635 }
1636
1637 #[test]
1638 fn escape_vscode_tab_null_cr_test() {
1639 let input = "now\t\0\ris";
1640 let expected = r#"now\x09\x00\x0Dis"#;
1641 let actual = escape_special_vscode_bytes(input).unwrap();
1642 assert_eq!(expected, actual);
1643 }
1644
1645 #[test]
1646 fn escape_vscode_multibyte_ok() {
1647 let input = "now🍪is";
1648 let actual = escape_special_vscode_bytes(input).unwrap();
1649 assert_eq!(input, actual);
1650 }
1651}