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