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