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