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 Config, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value,
29 config::NuCursorShape,
30 engine::{EngineState, Stack, StateWorkingSet},
31 report_shell_error,
32};
33use nu_utils::time::Instant;
34use nu_utils::{
35 filesystem::{PermissionResult, have_permission},
36 perf,
37};
38#[cfg(feature = "sqlite")]
39use reedline::SqliteBackedHistory;
40use reedline::{
41 CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
42 HistorySessionId, MouseClickMode, Osc133ClickEventsMarkers, Osc633Markers, Reedline,
43 SemanticPromptMarkers, Vi,
44};
45use std::sync::atomic::Ordering;
46use std::{
47 collections::HashMap,
48 env::temp_dir,
49 io::{self, IsTerminal, Write},
50 panic::{AssertUnwindSafe, catch_unwind},
51 path::{Path, PathBuf},
52 sync::Arc,
53 time::Duration,
54};
55use sysinfo::System;
56
57fn semantic_markers_from_config(
58 config: &Config,
59 term_program_is_vscode: bool,
60) -> Option<Box<dyn SemanticPromptMarkers>> {
61 if config.shell_integration.osc633 && term_program_is_vscode {
62 Some(Osc633Markers::boxed())
63 } else if config.shell_integration.osc133 {
64 Some(Osc133ClickEventsMarkers::boxed())
65 } else {
66 None
67 }
68}
69
70pub fn evaluate_repl(
72 engine_state: &mut EngineState,
73 stack: Stack,
74 prerun_command: Option<Spanned<String>>,
75 load_std_lib: Option<Spanned<String>>,
76 entire_start_time: Instant,
77 mode_dispatcher: Option<std::sync::Arc<std::sync::Mutex<Box<dyn crate::ModeDispatcher>>>>,
78) -> Result<()> {
79 let mut unique_stack = stack.clone();
85 let config = engine_state.get_config();
86 let use_color = config.use_ansi_coloring.get(engine_state);
87
88 let mut entry_num = 0;
89
90 let shell_integration_osc2 = config.shell_integration.osc2;
92 let shell_integration_osc7 = config.shell_integration.osc7;
93 let shell_integration_osc9_9 = config.shell_integration.osc9_9;
94 let shell_integration_osc633 = config.shell_integration.osc633;
95
96 let nu_prompt = NushellPrompt::new();
97
98 unique_stack.add_env_var(
100 "CMD_DURATION_MS".into(),
101 Value::string("0823", Span::unknown()),
102 );
103
104 unique_stack.set_last_exit_code(0, Span::unknown());
105
106 let mut line_editor = get_line_editor(engine_state, use_color)?;
107 let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
108
109 if let Some(s) = prerun_command {
110 eval_source(
111 engine_state,
112 &mut unique_stack,
113 s.item.as_bytes(),
114 &format!("repl_entry #{entry_num}"),
115 PipelineData::empty(),
116 false,
117 );
118 engine_state.merge_env(&mut unique_stack)?;
119 }
120
121 confirm_stdin_is_terminal()?;
122
123 let hostname = System::host_name();
124 if shell_integration_osc2 {
125 run_shell_integration_osc2(None, engine_state, &mut unique_stack, use_color);
126 }
127 if shell_integration_osc7 {
128 run_shell_integration_osc7(
129 hostname.as_deref(),
130 engine_state,
131 &mut unique_stack,
132 use_color,
133 );
134 }
135 if shell_integration_osc9_9 {
136 run_shell_integration_osc9_9(engine_state, &mut unique_stack, use_color);
137 }
138 if shell_integration_osc633 {
139 let cmd_text = line_editor.current_buffer_contents().to_string();
142
143 let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
144
145 run_shell_integration_osc633(
146 engine_state,
147 &mut unique_stack,
148 use_color,
149 replaced_cmd_text,
150 );
151 }
152
153 engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64);
154
155 engine_state.generate_nu_constant();
157
158 if load_std_lib.is_none() {
159 match engine_state.get_config().show_banner {
160 BannerKind::None => {}
161 BannerKind::Short => {
162 eval_source(
163 engine_state,
164 &mut unique_stack,
165 r#"banner --short"#.as_bytes(),
166 "show short banner",
167 PipelineData::empty(),
168 false,
169 );
170 }
171 BannerKind::Full => {
172 eval_source(
173 engine_state,
174 &mut unique_stack,
175 r#"banner"#.as_bytes(),
176 "show_banner",
177 PipelineData::empty(),
178 false,
179 );
180 }
181 }
182 }
183
184 kitty_protocol_healthcheck(engine_state);
185
186 let mut previous_engine_state = engine_state.clone();
188 let mut previous_stack_arc = Arc::new(unique_stack);
189 loop {
190 let mut current_engine_state = previous_engine_state.clone();
194 let current_stack = Stack::with_parent(previous_stack_arc.clone());
197 let temp_file_cloned = temp_file.clone();
198 let mut nu_prompt_cloned = nu_prompt.clone();
199
200 let iteration_panic_state = catch_unwind(AssertUnwindSafe(|| {
201 let (continue_loop, current_stack, line_editor) = loop_iteration(LoopContext {
202 engine_state: &mut current_engine_state,
203 stack: current_stack,
204 line_editor,
205 nu_prompt: &mut nu_prompt_cloned,
206 temp_file: &temp_file_cloned,
207 use_color,
208 entry_num: &mut entry_num,
209 hostname: hostname.as_deref(),
210 mode_dispatcher: mode_dispatcher.clone(),
211 });
212
213 (
215 continue_loop,
216 current_engine_state,
217 current_stack,
218 line_editor,
219 )
220 }));
221 match iteration_panic_state {
222 Ok((continue_loop, mut es, s, le)) => {
223 let mut merged_stack = Stack::with_changes_from_child(previous_stack_arc, s);
225
226 let prev_total_vars = previous_engine_state.num_vars();
228 let curr_total_vars = es.num_vars();
229 let new_variables_created = curr_total_vars > prev_total_vars;
230
231 if new_variables_created {
232 es.cleanup_stack_variables(&mut merged_stack);
234 }
235
236 previous_stack_arc = Arc::new(merged_stack);
237 previous_engine_state = es;
239 line_editor = le;
240 if !continue_loop {
241 break;
242 }
243 }
244 Err(_) => {
245 line_editor = get_line_editor(engine_state, use_color)?;
247 }
248 }
249 }
250
251 Ok(())
252}
253
254fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
255 let bytes = input
256 .chars()
257 .flat_map(|c| {
258 let mut buf = [0; 4]; let c_bytes = c.encode_utf8(&mut buf); if c_bytes.len() == 1 {
262 let byte = c_bytes.as_bytes()[0];
263
264 match byte {
265 b if b < 0x20 => format!("\\x{byte:02X}").into_bytes(),
267 b';' => "\\x3B".to_string().into_bytes(),
269 b'\\' => "\\\\".to_string().into_bytes(),
271 _ => vec![byte],
273 }
274 } else {
275 c_bytes.bytes().collect()
277 }
278 })
279 .collect();
280
281 String::from_utf8(bytes).map_err(|err| ShellError::CantConvert {
283 to_type: "string".to_string(),
284 from_type: "bytes".to_string(),
285 span: Span::unknown(),
286 help: Some(format!(
287 "Error {err}, Unable to convert {input} to escaped bytes"
288 )),
289 })
290}
291
292fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
293 let mut start_time = Instant::now();
294 let mut line_editor = Reedline::create();
295
296 store_history_id_in_engine(engine_state, &line_editor);
298 perf!("setup reedline", start_time, use_color);
299
300 if let Some(history) = engine_state.history_config() {
301 start_time = Instant::now();
302
303 line_editor = setup_history(engine_state, line_editor, history)?;
304
305 perf!("setup history", start_time, use_color);
306 }
307 Ok(line_editor)
308}
309
310struct LoopContext<'a> {
311 engine_state: &'a mut EngineState,
312 stack: Stack,
313 line_editor: Reedline,
314 nu_prompt: &'a mut NushellPrompt,
315 temp_file: &'a Path,
316 use_color: bool,
317 entry_num: &'a mut usize,
318 hostname: Option<&'a str>,
319 mode_dispatcher: Option<std::sync::Arc<std::sync::Mutex<Box<dyn crate::ModeDispatcher>>>>,
320}
321
322#[inline]
325fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
326 use nu_cmd_base::hook;
327 use reedline::Signal;
328 let loop_start_time = Instant::now();
329
330 let LoopContext {
331 engine_state,
332 mut stack,
333 line_editor,
334 nu_prompt,
335 temp_file,
336 use_color,
337 entry_num,
338 hostname,
339 mode_dispatcher,
340 } = ctx;
341
342 let mut start_time = Instant::now();
343 if let Err(err) = engine_state.merge_env(&mut stack) {
346 report_shell_error(None, engine_state, &err);
347 }
348 perf!("merge env", start_time, use_color);
349
350 start_time = Instant::now();
351 engine_state.reset_signals();
352 perf!("reset signals", start_time, use_color);
353
354 start_time = Instant::now();
355 if let Err(error) = hook::eval_env_change_hook(
358 &engine_state.get_config().hooks.env_change.clone(),
359 engine_state,
360 &mut stack,
361 ) {
362 report_shell_error(None, engine_state, &error)
363 }
364 perf!("env-change hook", start_time, use_color);
365
366 start_time = Instant::now();
367 if let Err(err) = hook::eval_hooks(
369 engine_state,
370 &mut stack,
371 vec![],
372 &engine_state.get_config().hooks.pre_prompt.clone(),
373 "pre_prompt",
374 ) {
375 report_shell_error(None, engine_state, &err);
376 }
377 perf!("pre-prompt hook", start_time, use_color);
378
379 let engine_reference = Arc::new(engine_state.clone());
380 let config = stack.get_config(engine_state);
381
382 start_time = Instant::now();
383 let cursor_config = CursorConfig {
385 vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
386 vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
387 emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
388 };
389 perf!("get config/cursor config", start_time, use_color);
390
391 start_time = Instant::now();
392 let stack_arc = Arc::new(stack);
396 let term_program_is_vscode = engine_state
397 .get_env_var("TERM_PROGRAM")
398 .and_then(|v| v.as_str().ok())
399 == Some("vscode");
400 let mut line_editor = line_editor
401 .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
402 .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
405 .with_highlighter({
407 let shannon_mode = stack_arc.get_env_var(engine_state, "SHANNON_MODE")
408 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
409 .unwrap_or_else(|| "nu".to_string());
410 if shannon_mode == "bash" {
411 Box::new(crate::bash_highlight::BashHighlighter::new(&config))
412 as Box<dyn reedline::Highlighter>
413 } else if shannon_mode != "nu" {
414 Box::<NoOpHighlighter>::default() as Box<dyn reedline::Highlighter>
415 } else {
416 Box::new(NuHighlighter {
417 engine_state: engine_reference.clone(),
418 stack: stack_arc.clone(),
420 }) as Box<dyn reedline::Highlighter>
421 }
422 })
423 .with_validator(Box::new(NuValidator {
424 engine_state: engine_reference.clone(),
425 }))
426 .with_completer(Box::new(NuCompleter::new(
427 engine_reference.clone(),
428 stack_arc.clone(),
430 )))
431 .with_quick_completions(config.completions.quick)
432 .with_partial_completions(config.completions.partial)
433 .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
434 .with_cwd(Some(
435 engine_state
436 .cwd(None)
437 .map(|cwd| cwd.into_std_path_buf())
438 .unwrap_or_default()
439 .to_string_lossy()
440 .to_string(),
441 ))
442 .with_cursor_config(cursor_config)
443 .with_visual_selection_style(nu_ansi_term::Style {
444 is_reverse: true,
445 ..Default::default()
446 })
447 .with_semantic_markers(semantic_markers_from_config(
448 &config,
449 term_program_is_vscode,
450 ))
451 .with_mouse_click(if config.shell_integration.osc133 {
452 MouseClickMode::Enabled
453 } else {
454 MouseClickMode::Disabled
455 });
456
457 perf!("reedline builder", start_time, use_color);
458
459 let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
460
461 start_time = Instant::now();
462 line_editor = if config.use_ansi_coloring.get(engine_state) && config.show_hints {
463 line_editor.with_hinter(Box::new({
464 let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
466 CwdAwareHinter::default().with_style(style)
467 }))
468 } else {
469 line_editor.disable_hints()
470 };
471
472 perf!("reedline coloring/style_computer", start_time, use_color);
473
474 start_time = Instant::now();
475 trace!("adding menus");
476 line_editor =
477 add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
478 report_shell_error(None, engine_state, &e);
479 Reedline::create()
480 });
481
482 perf!("reedline adding menus", start_time, use_color);
483
484 start_time = Instant::now();
485 let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
487
488 line_editor = if let Ok((cmd, args)) = buffer_editor {
489 let mut command = std::process::Command::new(cmd);
490 let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
491 warn!("Couldn't convert environment variable values to strings: {e}");
492 HashMap::default()
493 });
494 command.args(args).envs(envs);
495 line_editor.with_buffer_editor(command, temp_file.to_path_buf())
496 } else {
497 line_editor
498 };
499
500 perf!("reedline buffer_editor", start_time, use_color);
501
502 if let Some(history) = engine_state.history_config() {
503 start_time = Instant::now();
504
505 line_editor = line_editor
506 .with_history_exclusion_prefix(history.ignore_space_prefixed.then_some(" ".into()));
507
508 if history.sync_on_enter
509 && let Err(e) = line_editor.sync_history()
510 {
511 warn!("Failed to sync history: {e}");
512 }
513
514 perf!("sync_history", start_time, use_color);
515 }
516
517 start_time = Instant::now();
518 line_editor = setup_keybindings(engine_state, line_editor);
520
521 perf!("keybindings", start_time, use_color);
522
523 start_time = Instant::now();
524 let config = &engine_state.get_config().clone();
525 prompt_update::update_prompt(
526 config,
527 engine_state,
528 &mut Stack::with_parent(stack_arc.clone()),
529 nu_prompt,
530 );
531 let transient_prompt = prompt_update::make_transient_prompt(
532 config,
533 engine_state,
534 &mut Stack::with_parent(stack_arc.clone()),
535 nu_prompt,
536 );
537
538 perf!("update_prompt", start_time, use_color);
539
540 *entry_num += 1;
541
542 start_time = Instant::now();
543 line_editor = line_editor.with_transient_prompt(transient_prompt);
544 let input = line_editor.read_line(nu_prompt);
545 line_editor = line_editor
548 .with_highlighter(Box::<NoOpHighlighter>::default())
550 .with_completer(Box::<DefaultCompleter>::default())
552 .with_immediately_accept(false);
554
555 let shell_integration_osc2 = config.shell_integration.osc2;
557 let shell_integration_osc7 = config.shell_integration.osc7;
558 let shell_integration_osc9_9 = config.shell_integration.osc9_9;
559 let shell_integration_osc133 = config.shell_integration.osc133;
560 let shell_integration_osc633 = config.shell_integration.osc633;
561 let shell_integration_reset_application_mode = config.shell_integration.reset_application_mode;
562
563 let mut stack = Arc::unwrap_or_clone(stack_arc);
566
567 perf!("line_editor setup", start_time, use_color);
568
569 let line_editor_input_time = Instant::now();
570 match input {
571 Ok(Signal::Success(repl_cmd_line_text)) => {
572 if repl_cmd_line_text.trim() == "__shannon_switch" {
574 if let Some(ref _dispatcher) = mode_dispatcher {
575 let current = stack
576 .get_env_var(engine_state, "SHANNON_MODE")
577 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
578 .unwrap_or_else(|| "nu".to_string());
579 let modes = ["nu", "bash"];
580 let next_idx = modes.iter().position(|m| *m == current)
581 .map(|i| (i + 1) % modes.len())
582 .unwrap_or(0);
583 stack.add_env_var(
584 "SHANNON_MODE".to_string(),
585 Value::string(modes[next_idx], Span::unknown()),
586 );
587 }
588 return (true, stack, line_editor);
589 }
590 if repl_cmd_line_text.trim() == "exit" {
592 if let Some(ref _dispatcher) = mode_dispatcher {
593 let mode = stack
594 .get_env_var(engine_state, "SHANNON_MODE")
595 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
596 .unwrap_or_else(|| "nu".to_string());
597 if mode != "nu" {
598 return (false, stack, line_editor);
599 }
600 }
601 }
602
603 let history_supports_meta = match engine_state.history_config().map(|h| h.file_format) {
604 #[cfg(feature = "sqlite")]
605 Some(HistoryFileFormat::Sqlite) => true,
606 _ => false,
607 };
608
609 if history_supports_meta {
610 prepare_history_metadata(
611 &repl_cmd_line_text,
612 hostname,
613 engine_state,
614 &mut line_editor,
615 );
616 }
617
618 start_time = Instant::now();
620
621 {
624 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
626 repl.buffer = repl_cmd_line_text.to_string();
627 drop(repl);
628
629 if let Err(err) = hook::eval_hooks(
630 engine_state,
631 &mut stack,
632 vec![],
633 &engine_state.get_config().hooks.pre_execution.clone(),
634 "pre_execution",
635 ) {
636 report_shell_error(None, engine_state, &err);
637 }
638 }
639
640 perf!("pre_execution_hook", start_time, use_color);
641
642 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
643 repl.cursor_pos = line_editor.current_insertion_point();
644 repl.buffer = line_editor.current_buffer_contents().to_string();
645 drop(repl);
646
647 if shell_integration_osc633 {
648 if stack
649 .get_env_var(engine_state, "TERM_PROGRAM")
650 .and_then(|v| v.as_str().ok())
651 == Some("vscode")
652 {
653 start_time = Instant::now();
654
655 run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
656
657 perf!(
658 "pre_execute_marker (633;C) ansi escape sequence",
659 start_time,
660 use_color
661 );
662 } else if shell_integration_osc133 {
663 start_time = Instant::now();
664
665 run_ansi_sequence(PRE_EXECUTION_MARKER);
666
667 perf!(
668 "pre_execute_marker (133;C) ansi escape sequence",
669 start_time,
670 use_color
671 );
672 }
673 } else if shell_integration_osc133 {
674 start_time = Instant::now();
675
676 run_ansi_sequence(PRE_EXECUTION_MARKER);
677
678 perf!(
679 "pre_execute_marker (133;C) ansi escape sequence",
680 start_time,
681 use_color
682 );
683 }
684
685 let cmd_execution_start_time = Instant::now();
687
688 let mut shannon_handled = false;
690 if let Some(ref dispatcher) = mode_dispatcher {
691 let mode = stack
692 .get_env_var(engine_state, "SHANNON_MODE")
693 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
694 .unwrap_or_else(|| "nu".to_string());
695 if mode != "nu" {
696 #[allow(deprecated)]
697 let env_strings = env_to_strings(engine_state, &stack).unwrap_or_default();
698 let cwd = stack
699 .get_env_var(engine_state, "PWD")
700 .and_then(|v| v.as_str().ok())
701 .map(std::path::PathBuf::from)
702 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
703 let result = dispatcher.lock().unwrap().execute(
704 &mode,
705 &repl_cmd_line_text,
706 env_strings,
707 cwd,
708 );
709 for (key, value) in &result.env {
710 stack.add_env_var(
711 key.clone(),
712 Value::string(value, Span::unknown()),
713 );
714 }
715 let _ = stack.set_cwd(&result.cwd);
716 let _ = std::env::set_current_dir(&result.cwd);
717 shannon_handled = true;
718 }
719 }
720
721 if !shannon_handled {
722 match parse_operation(repl_cmd_line_text.clone(), engine_state, &stack) {
723 Ok(operation) => match operation {
724 ReplOperation::AutoCd { cwd, target, span } => {
725 do_auto_cd(target, cwd, &mut stack, engine_state, span);
726
727 run_finaliziation_ansi_sequence(
728 &stack,
729 engine_state,
730 use_color,
731 shell_integration_osc633,
732 shell_integration_osc133,
733 );
734 }
735 ReplOperation::RunCommand(cmd) => {
736 line_editor = do_run_cmd(
737 &cmd,
738 &mut stack,
739 engine_state,
740 line_editor,
741 shell_integration_osc2,
742 *entry_num,
743 use_color,
744 );
745
746 run_finaliziation_ansi_sequence(
747 &stack,
748 engine_state,
749 use_color,
750 shell_integration_osc633,
751 shell_integration_osc133,
752 );
753 }
754 ReplOperation::DoNothing => {}
756 },
757 Err(ref e) => error!("Error parsing operation: {e}"),
758 }
759 } let cmd_duration = cmd_execution_start_time.elapsed();
761
762 stack.add_env_var(
764 "CMD_DURATION_MS".into(),
765 Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
766 );
767
768 if history_supports_meta
769 && let Err(e) = fill_in_result_related_history_metadata(
770 &repl_cmd_line_text,
771 engine_state,
772 cmd_duration,
773 &mut stack,
774 &mut line_editor,
775 )
776 {
777 warn!("Could not fill in result related history metadata: {e}");
778 }
779
780 if shell_integration_osc2 {
781 run_shell_integration_osc2(None, engine_state, &mut stack, use_color);
782 }
783 if shell_integration_osc7 {
784 run_shell_integration_osc7(hostname, engine_state, &mut stack, use_color);
785 }
786 if shell_integration_osc9_9 {
787 run_shell_integration_osc9_9(engine_state, &mut stack, use_color);
788 }
789 if shell_integration_osc633 {
790 run_shell_integration_osc633(
791 engine_state,
792 &mut stack,
793 use_color,
794 repl_cmd_line_text,
795 );
796 }
797 if shell_integration_reset_application_mode {
798 run_shell_integration_reset_application_mode();
799 }
800
801 line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
802 }
803 Ok(Signal::CtrlC) => {
804 run_finaliziation_ansi_sequence(
806 &stack,
807 engine_state,
808 use_color,
809 shell_integration_osc633,
810 shell_integration_osc133,
811 );
812 }
813 Ok(Signal::CtrlD) => {
814 run_finaliziation_ansi_sequence(
817 &stack,
818 engine_state,
819 use_color,
820 shell_integration_osc633,
821 shell_integration_osc133,
822 );
823
824 println!();
825
826 cleanup_exit((), engine_state, 0);
827
828 return (true, stack, line_editor);
830 }
831 Err(err) => {
832 let message = err.to_string();
833 if !message.contains("duration") {
834 eprintln!("Error: {err:?}");
835 }
840
841 run_finaliziation_ansi_sequence(
842 &stack,
843 engine_state,
844 use_color,
845 shell_integration_osc633,
846 shell_integration_osc133,
847 );
848 }
849 Ok(_) => {
850 }
852 }
853 perf!(
854 "processing line editor input",
855 line_editor_input_time,
856 use_color
857 );
858
859 perf!(
860 "time between prompts in line editor loop",
861 loop_start_time,
862 use_color
863 );
864
865 (true, stack, line_editor)
866}
867
868fn prepare_history_metadata(
872 s: &str,
873 hostname: Option<&str>,
874 engine_state: &EngineState,
875 line_editor: &mut Reedline,
876) {
877 if !s.is_empty() && line_editor.has_last_command_context() {
878 let result = line_editor
879 .update_last_command_context(&|mut c| {
880 c.start_timestamp = Some(chrono::Utc::now());
881 c.hostname = hostname.map(str::to_string);
882 c.cwd = engine_state
883 .cwd(None)
884 .ok()
885 .map(|path| path.to_string_lossy().to_string());
886 c
887 })
888 .into_diagnostic();
889 if let Err(e) = result {
890 warn!("Could not prepare history metadata: {e}");
891 }
892 }
893}
894
895fn fill_in_result_related_history_metadata(
899 s: &str,
900 engine_state: &EngineState,
901 cmd_duration: Duration,
902 stack: &mut Stack,
903 line_editor: &mut Reedline,
904) -> Result<()> {
905 if !s.is_empty() && line_editor.has_last_command_context() {
906 line_editor
907 .update_last_command_context(&|mut c| {
908 c.duration = Some(cmd_duration);
909 c.exit_status = stack
910 .get_env_var(engine_state, "LAST_EXIT_CODE")
911 .and_then(|e| e.as_int().ok());
912 c
913 })
914 .into_diagnostic()?; }
916 Ok(())
917}
918
919enum ReplOperation {
921 AutoCd {
923 cwd: String,
925 target: PathBuf,
927 span: Span,
929 },
930 RunCommand(String),
932 DoNothing,
934}
935
936fn parse_operation(
944 s: String,
945 engine_state: &EngineState,
946 stack: &Stack,
947) -> Result<ReplOperation, ErrReport> {
948 let tokens = lex(s.as_bytes(), 0, &[], &[], false);
949 let cwd = engine_state
951 .cwd(Some(stack))
952 .map(|p| p.to_string_lossy().to_string())
953 .unwrap_or_default();
954 let mut orig = s.clone();
955 if orig.starts_with('`') {
956 orig = trim_quotes_str(&orig).to_string()
957 }
958
959 let path = nu_path::expand_path_with(&orig, &cwd, true);
960 if (engine_state.get_config().auto_cd_implicit || looks_like_path(&orig))
961 && path.is_dir()
962 && tokens.0.len() == 1
963 {
964 Ok(ReplOperation::AutoCd {
965 cwd,
966 target: path,
967 span: tokens.0[0].span,
968 })
969 } else if !s.trim().is_empty() {
970 Ok(ReplOperation::RunCommand(s))
971 } else {
972 Ok(ReplOperation::DoNothing)
973 }
974}
975
976fn do_auto_cd(
980 path: PathBuf,
981 cwd: String,
982 stack: &mut Stack,
983 engine_state: &mut EngineState,
984 span: Span,
985) {
986 let path = {
987 if !path.exists() {
988 report_shell_error(
989 Some(stack),
990 engine_state,
991 &ShellError::Io(IoError::new_with_additional_context(
992 shell_error::io::ErrorKind::DirectoryNotFound,
993 span,
994 PathBuf::from(&path),
995 "Cannot change directory",
996 )),
997 );
998 }
999 path.to_string_lossy().to_string()
1000 };
1001
1002 if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
1003 report_shell_error(
1004 Some(stack),
1005 engine_state,
1006 &ShellError::Io(IoError::new_with_additional_context(
1007 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
1008 span,
1009 PathBuf::from(path),
1010 "Cannot change directory",
1011 )),
1012 );
1013 return;
1014 }
1015
1016 stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), span));
1017
1018 if let Err(err) = stack.set_cwd(&path) {
1021 report_shell_error(Some(stack), engine_state, &err);
1022 return;
1023 };
1024 let cwd = Value::string(cwd, span);
1025
1026 let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
1027 let mut shells = if let Some(v) = shells {
1028 v.clone().into_list().unwrap_or_else(|_| vec![cwd])
1029 } else {
1030 vec![cwd]
1031 };
1032
1033 let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
1034 let current_shell = if let Some(v) = current_shell {
1035 v.as_int().unwrap_or_default() as usize
1036 } else {
1037 0
1038 };
1039
1040 let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
1041 let last_shell = if let Some(v) = last_shell {
1042 v.as_int().unwrap_or_default() as usize
1043 } else {
1044 0
1045 };
1046
1047 shells[current_shell] = Value::string(path, span);
1048
1049 stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
1050 stack.add_env_var(
1051 "NUSHELL_LAST_SHELL".into(),
1052 Value::int(last_shell as i64, span),
1053 );
1054 stack.set_last_exit_code(0, span);
1055}
1056
1057fn do_run_cmd(
1062 s: &str,
1063 stack: &mut Stack,
1064 engine_state: &mut EngineState,
1065 line_editor: Reedline,
1068 shell_integration_osc2: bool,
1069 entry_num: usize,
1070 use_color: bool,
1071) -> Reedline {
1072 trace!("eval source: {s}");
1073
1074 let mut cmds = s.split_whitespace();
1075
1076 let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
1077
1078 if let Some("exit") = cmds.next() {
1079 let mut working_set = StateWorkingSet::new(engine_state);
1080 let _ = parse(&mut working_set, None, s.as_bytes(), false);
1081
1082 if working_set.parse_errors.is_empty() {
1083 match cmds.next() {
1084 Some(s) => {
1085 if let Ok(n) = s.parse::<i32>() {
1086 return cleanup_exit(line_editor, engine_state, n);
1087 }
1088 }
1089 None => {
1090 return cleanup_exit(line_editor, engine_state, 0);
1091 }
1092 }
1093 }
1094 }
1095
1096 if shell_integration_osc2 {
1097 run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
1098 }
1099
1100 eval_source(
1101 engine_state,
1102 stack,
1103 s.as_bytes(),
1104 &format!("repl_entry #{entry_num}"),
1105 PipelineData::empty(),
1106 false,
1107 );
1108
1109 if had_warning_before && engine_state.is_interactive {
1112 engine_state
1113 .exit_warning_given
1114 .store(false, Ordering::SeqCst);
1115 }
1116
1117 line_editor
1118}
1119
1120fn run_shell_integration_osc2(
1126 command_name: Option<&str>,
1127 engine_state: &EngineState,
1128 stack: &mut Stack,
1129 use_color: bool,
1130) {
1131 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1132 let start_time = Instant::now();
1133
1134 let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1136 let home_dir_str = p.as_path().display().to_string();
1137 if path.starts_with(&home_dir_str) {
1138 path.replacen(&home_dir_str, "~", 1)
1139 } else {
1140 path
1141 }
1142 } else {
1143 path
1144 };
1145
1146 let title = match command_name {
1147 Some(binary_name) => {
1148 let split_binary_name = binary_name.split_whitespace().next();
1149 if let Some(binary_name) = split_binary_name {
1150 format!("{maybe_abbrev_path}> {binary_name}")
1151 } else {
1152 maybe_abbrev_path.to_string()
1153 }
1154 }
1155 None => maybe_abbrev_path.to_string(),
1156 };
1157
1158 run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1164
1165 perf!("set title with command osc2", start_time, use_color);
1166 }
1167}
1168
1169fn run_shell_integration_osc7(
1170 hostname: Option<&str>,
1171 engine_state: &EngineState,
1172 stack: &mut Stack,
1173 use_color: bool,
1174) {
1175 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1176 let start_time = Instant::now();
1177
1178 let path = if cfg!(windows) {
1179 path.replace('\\', "/")
1180 } else {
1181 path
1182 };
1183
1184 run_ansi_sequence(&format!(
1186 "\x1b]7;file://{}{}{}\x1b\\",
1187 percent_encoding::utf8_percent_encode(
1188 hostname.unwrap_or("localhost"),
1189 percent_encoding::CONTROLS
1190 ),
1191 if path.starts_with('/') { "" } else { "/" },
1192 percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1193 ));
1194
1195 perf!(
1196 "communicate path to terminal with osc7",
1197 start_time,
1198 use_color
1199 );
1200 }
1201}
1202
1203fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1204 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1205 let start_time = Instant::now();
1206
1207 run_ansi_sequence(&format!("\x1b]9;9;{}\x1b\\", path));
1210
1211 perf!(
1212 "communicate path to terminal with osc9;9",
1213 start_time,
1214 use_color
1215 );
1216 }
1217}
1218
1219fn run_shell_integration_osc633(
1220 engine_state: &EngineState,
1221 stack: &mut Stack,
1222 use_color: bool,
1223 repl_cmd_line_text: String,
1224) {
1225 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1226 if stack
1229 .get_env_var(engine_state, "TERM_PROGRAM")
1230 .and_then(|v| v.as_str().ok())
1231 == Some("vscode")
1232 {
1233 let start_time = Instant::now();
1234
1235 run_ansi_sequence(&format!(
1238 "{VSCODE_CWD_PROPERTY_MARKER_PREFIX}{path}{VSCODE_CWD_PROPERTY_MARKER_SUFFIX}"
1239 ));
1240
1241 perf!(
1242 "communicate path to terminal with osc633;P",
1243 start_time,
1244 use_color
1245 );
1246
1247 let replaced_cmd_text =
1250 escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1251
1252 run_ansi_sequence(&format!(
1254 "{VSCODE_COMMANDLINE_MARKER_PREFIX}{replaced_cmd_text}{VSCODE_COMMANDLINE_MARKER_SUFFIX}"
1255 ));
1256 }
1257 }
1258}
1259
1260fn run_shell_integration_reset_application_mode() {
1261 run_ansi_sequence(RESET_APPLICATION_MODE);
1262}
1263
1264fn flush_engine_state_repl_buffer(
1268 engine_state: &mut EngineState,
1269 mut line_editor: Reedline,
1270) -> Reedline {
1271 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1272 line_editor.run_edit_commands(&[
1273 EditCommand::Clear,
1274 EditCommand::InsertString(repl.buffer.to_string()),
1275 EditCommand::MoveToPosition {
1276 position: repl.cursor_pos,
1277 select: false,
1278 },
1279 ]);
1280 if repl.accept {
1281 line_editor = line_editor.with_immediately_accept(true)
1282 }
1283 repl.accept = false;
1284 repl.buffer = "".to_string();
1285 repl.cursor_pos = 0;
1286 line_editor
1287}
1288
1289fn setup_history(
1293 engine_state: &mut EngineState,
1294 line_editor: Reedline,
1295 history: HistoryConfig,
1296) -> Result<Reedline> {
1297 let history_session_id = if history.isolation {
1299 Reedline::create_history_session_id()
1300 } else {
1301 None
1302 };
1303
1304 if let Some(path) = history.file_path() {
1305 return update_line_editor_history(
1306 engine_state,
1307 path,
1308 history,
1309 line_editor,
1310 history_session_id,
1311 );
1312 };
1313 Ok(line_editor)
1314}
1315
1316fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1320 match create_keybindings(engine_state.get_config()) {
1321 Ok(keybindings) => match keybindings {
1322 KeybindingsMode::Emacs(mut keybindings) => {
1323 keybindings.add_binding(
1325 crossterm::event::KeyModifiers::SHIFT,
1326 crossterm::event::KeyCode::BackTab,
1327 reedline::ReedlineEvent::ExecuteHostCommand("__shannon_switch".into()),
1328 );
1329 let edit_mode = Box::new(Emacs::new(keybindings));
1330 line_editor.with_edit_mode(edit_mode)
1331 }
1332 KeybindingsMode::Vi {
1333 mut insert_keybindings,
1334 mut normal_keybindings,
1335 } => {
1336 for kb in [&mut insert_keybindings, &mut normal_keybindings] {
1338 kb.add_binding(
1339 crossterm::event::KeyModifiers::SHIFT,
1340 crossterm::event::KeyCode::BackTab,
1341 reedline::ReedlineEvent::ExecuteHostCommand("__shannon_switch".into()),
1342 );
1343 }
1344 let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1345 line_editor.with_edit_mode(edit_mode)
1346 }
1347 },
1348 Err(e) => {
1349 report_shell_error(None, engine_state, &e);
1350 line_editor
1351 }
1352 }
1353}
1354
1355fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1359 if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1360 warn!("Terminal doesn't support use_kitty_protocol config");
1361 }
1362}
1363
1364fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1365 let session_id = line_editor
1366 .get_history_session_id()
1367 .map(i64::from)
1368 .unwrap_or(0);
1369
1370 engine_state.history_session_id = session_id;
1371}
1372
1373fn update_line_editor_history(
1374 engine_state: &mut EngineState,
1375 history_path: PathBuf,
1376 history: HistoryConfig,
1377 line_editor: Reedline,
1378 history_session_id: Option<HistorySessionId>,
1379) -> Result<Reedline, ErrReport> {
1380 let ignore_space_prefixed = history.ignore_space_prefixed;
1381 let history: Box<dyn reedline::History> = match history.file_format {
1382 HistoryFileFormat::Plaintext => Box::new(
1383 FileBackedHistory::with_file(history.max_size as usize, history_path)
1384 .into_diagnostic()?,
1385 ),
1386 #[cfg(not(feature = "sqlite"))]
1388 HistoryFileFormat::Sqlite => {
1389 return Err(miette::miette!(
1390 help = "compile Nushell with the `sqlite` feature to use this",
1391 "Unsupported history file format",
1392 ));
1393 }
1394 #[cfg(feature = "sqlite")]
1395 HistoryFileFormat::Sqlite => Box::new(
1396 SqliteBackedHistory::with_file(
1397 history_path.to_path_buf(),
1398 history_session_id,
1399 Some(chrono::Utc::now()),
1400 )
1401 .into_diagnostic()?,
1402 ),
1403 };
1404 let line_editor = line_editor
1405 .with_history_session_id(history_session_id)
1406 .with_history_exclusion_prefix(ignore_space_prefixed.then_some(" ".into()))
1407 .with_history(history);
1408
1409 store_history_id_in_engine(engine_state, &line_editor);
1410
1411 Ok(line_editor)
1412}
1413
1414fn confirm_stdin_is_terminal() -> Result<()> {
1415 if !std::io::stdin().is_terminal() {
1418 return Err(std::io::Error::new(
1419 std::io::ErrorKind::NotFound,
1420 "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1421 ))
1422 .into_diagnostic();
1423 }
1424 Ok(())
1425}
1426fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1427 match shape {
1428 NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1429 NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1430 NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1431 NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1432 NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1433 NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1434 NuCursorShape::Inherit => None,
1435 }
1436}
1437
1438fn get_command_finished_marker(
1439 stack: &Stack,
1440 engine_state: &EngineState,
1441 shell_integration_osc633: bool,
1442 shell_integration_osc133: bool,
1443) -> String {
1444 let exit_code = stack
1445 .get_env_var(engine_state, "LAST_EXIT_CODE")
1446 .and_then(|e| e.as_int().ok());
1447
1448 if shell_integration_osc633 {
1449 if stack
1450 .get_env_var(engine_state, "TERM_PROGRAM")
1451 .and_then(|v| v.as_str().ok())
1452 == Some("vscode")
1453 {
1454 format!(
1456 "{}{}{}",
1457 VSCODE_POST_EXECUTION_MARKER_PREFIX,
1458 exit_code.unwrap_or(0),
1459 VSCODE_POST_EXECUTION_MARKER_SUFFIX
1460 )
1461 } else if shell_integration_osc133 {
1462 format!(
1464 "{}{}{}",
1465 POST_EXECUTION_MARKER_PREFIX,
1466 exit_code.unwrap_or(0),
1467 POST_EXECUTION_MARKER_SUFFIX
1468 )
1469 } else {
1470 "\x1b[0m".to_string()
1472 }
1473 } else if shell_integration_osc133 {
1474 format!(
1475 "{}{}{}",
1476 POST_EXECUTION_MARKER_PREFIX,
1477 exit_code.unwrap_or(0),
1478 POST_EXECUTION_MARKER_SUFFIX
1479 )
1480 } else {
1481 "\x1b[0m".to_string()
1482 }
1483}
1484
1485fn run_ansi_sequence(seq: &str) {
1486 if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1487 warn!("Error writing ansi sequence {e}");
1488 } else if let Err(e) = io::stdout().flush() {
1489 warn!("Error flushing stdio {e}");
1490 }
1491}
1492
1493fn run_finaliziation_ansi_sequence(
1494 stack: &Stack,
1495 engine_state: &EngineState,
1496 use_color: bool,
1497 shell_integration_osc633: bool,
1498 shell_integration_osc133: bool,
1499) {
1500 if shell_integration_osc633 {
1501 if stack
1503 .get_env_var(engine_state, "TERM_PROGRAM")
1504 .and_then(|v| v.as_str().ok())
1505 == Some("vscode")
1506 {
1507 let start_time = Instant::now();
1508
1509 run_ansi_sequence(&get_command_finished_marker(
1510 stack,
1511 engine_state,
1512 shell_integration_osc633,
1513 shell_integration_osc133,
1514 ));
1515
1516 perf!(
1517 "post_execute_marker (633;D) ansi escape sequences",
1518 start_time,
1519 use_color
1520 );
1521 } else if shell_integration_osc133 {
1522 let start_time = Instant::now();
1523
1524 run_ansi_sequence(&get_command_finished_marker(
1525 stack,
1526 engine_state,
1527 shell_integration_osc633,
1528 shell_integration_osc133,
1529 ));
1530
1531 perf!(
1532 "post_execute_marker (133;D) ansi escape sequences",
1533 start_time,
1534 use_color
1535 );
1536 }
1537 } else if shell_integration_osc133 {
1538 let start_time = Instant::now();
1539
1540 run_ansi_sequence(&get_command_finished_marker(
1541 stack,
1542 engine_state,
1543 shell_integration_osc633,
1544 shell_integration_osc133,
1545 ));
1546
1547 perf!(
1548 "post_execute_marker (133;D) ansi escape sequences",
1549 start_time,
1550 use_color
1551 );
1552 }
1553}
1554
1555#[cfg(windows)]
1557static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1558 fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1559});
1560
1561fn looks_like_path(orig: &str) -> bool {
1563 #[cfg(windows)]
1564 {
1565 if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1566 return true;
1567 }
1568 }
1569
1570 orig.starts_with('.')
1571 || orig.starts_with('~')
1572 || orig.starts_with('/')
1573 || orig.starts_with('\\')
1574 || orig.ends_with(std::path::MAIN_SEPARATOR)
1575}
1576
1577#[cfg(test)]
1578mod semantic_marker_tests {
1579 use super::semantic_markers_from_config;
1580 use nu_protocol::Config;
1581 use reedline::PromptKind;
1582
1583 #[test]
1584 fn semantic_markers_use_osc633_in_vscode() {
1585 let mut config = Config::default();
1586 config.shell_integration.osc633 = true;
1587 config.shell_integration.osc133 = true;
1588
1589 let markers =
1590 semantic_markers_from_config(&config, true).expect("expected semantic markers");
1591
1592 assert_eq!(
1593 markers.prompt_start(PromptKind::Primary).as_ref(),
1594 "\x1b]633;A;k=i\x1b\\"
1595 );
1596 }
1597
1598 #[test]
1599 fn semantic_markers_use_osc133_click_events() {
1600 let mut config = Config::default();
1601 config.shell_integration.osc133 = true;
1602
1603 let markers =
1604 semantic_markers_from_config(&config, false).expect("expected semantic markers");
1605
1606 assert_eq!(
1607 markers.prompt_start(PromptKind::Primary).as_ref(),
1608 "\x1b]133;A;k=i;click_events=1\x1b\\"
1609 );
1610 }
1611
1612 #[test]
1613 fn semantic_markers_none_when_disabled() {
1614 let mut config = Config::default();
1615 config.shell_integration.osc133 = false;
1616 config.shell_integration.osc633 = false;
1617 assert!(semantic_markers_from_config(&config, false).is_none());
1618 }
1619}
1620
1621#[cfg(windows)]
1622#[test]
1623fn looks_like_path_windows_drive_path_works() {
1624 assert!(looks_like_path("C:"));
1625 assert!(looks_like_path("D:\\"));
1626 assert!(looks_like_path("E:/"));
1627 assert!(looks_like_path("F:\\some_dir"));
1628 assert!(looks_like_path("G:/some_dir"));
1629}
1630
1631#[cfg(windows)]
1632#[test]
1633fn trailing_slash_looks_like_path() {
1634 assert!(looks_like_path("foo\\"))
1635}
1636
1637#[cfg(not(windows))]
1638#[test]
1639fn trailing_slash_looks_like_path() {
1640 assert!(looks_like_path("foo/"))
1641}
1642
1643#[test]
1644fn are_session_ids_in_sync() {
1645 let engine_state = &mut EngineState::new();
1646 let history = engine_state.history_config().unwrap();
1647 let history_path = history.file_path().unwrap();
1648 let line_editor = reedline::Reedline::create();
1649 let history_session_id = reedline::Reedline::create_history_session_id();
1650 let line_editor = update_line_editor_history(
1651 engine_state,
1652 history_path,
1653 history,
1654 line_editor,
1655 history_session_id,
1656 );
1657 assert_eq!(
1658 i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1659 engine_state.history_session_id
1660 );
1661}
1662
1663#[cfg(test)]
1664mod test_auto_cd {
1665 use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1666 use nu_path::AbsolutePath;
1667 use nu_protocol::engine::{EngineState, Stack};
1668 use tempfile::tempdir;
1669
1670 #[cfg(any(unix, windows))]
1672 fn symlink(
1673 original: impl AsRef<AbsolutePath>,
1674 link: impl AsRef<AbsolutePath>,
1675 ) -> std::io::Result<()> {
1676 let original = original.as_ref();
1677 let link = link.as_ref();
1678
1679 #[cfg(unix)]
1680 {
1681 std::os::unix::fs::symlink(original, link)
1682 }
1683 #[cfg(windows)]
1684 {
1685 if original.is_dir() {
1686 std::os::windows::fs::symlink_dir(original, link)
1687 } else {
1688 std::os::windows::fs::symlink_file(original, link)
1689 }
1690 }
1691 }
1692
1693 #[track_caller]
1697 fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1698 let mut engine_state = EngineState::new();
1700 let mut stack = Stack::new();
1701 stack.set_cwd(before.as_ref()).unwrap();
1702
1703 let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1705 let ReplOperation::AutoCd { cwd, target, span } = op else {
1706 panic!("'{input}' was not parsed into an auto-cd operation")
1707 };
1708
1709 do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1711 let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1712
1713 let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1717 let after = std::fs::canonicalize(after.as_ref()).unwrap();
1718 assert_eq!(updated_cwd, after);
1719 }
1720
1721 #[test]
1722 fn auto_cd_root() {
1723 let tempdir = tempdir().unwrap();
1724 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1725
1726 let input = if cfg!(windows) { r"C:\" } else { "/" };
1727 let root = AbsolutePath::try_new(input).unwrap();
1728 check(tempdir, input, root);
1729 }
1730
1731 #[test]
1732 fn auto_cd_tilde() {
1733 let tempdir = tempdir().unwrap();
1734 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1735
1736 let home = nu_path::home_dir().unwrap();
1737 check(tempdir, "~", home);
1738 }
1739
1740 #[test]
1741 fn auto_cd_dot() {
1742 let tempdir = tempdir().unwrap();
1743 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1744
1745 check(tempdir, ".", tempdir);
1746 }
1747
1748 #[test]
1749 fn auto_cd_double_dot() {
1750 let tempdir = tempdir().unwrap();
1751 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1752
1753 let dir = tempdir.join("foo");
1754 std::fs::create_dir_all(&dir).unwrap();
1755 check(dir, "..", tempdir);
1756 }
1757
1758 #[test]
1759 fn auto_cd_triple_dot() {
1760 let tempdir = tempdir().unwrap();
1761 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1762
1763 let dir = tempdir.join("foo").join("bar");
1764 std::fs::create_dir_all(&dir).unwrap();
1765 check(dir, "...", tempdir);
1766 }
1767
1768 #[test]
1769 fn auto_cd_relative() {
1770 let tempdir = tempdir().unwrap();
1771 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1772
1773 let foo = tempdir.join("foo");
1774 let bar = tempdir.join("bar");
1775 std::fs::create_dir_all(&foo).unwrap();
1776 std::fs::create_dir_all(&bar).unwrap();
1777 let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1778 check(foo, input, bar);
1779 }
1780
1781 #[test]
1782 fn auto_cd_trailing_slash() {
1783 let tempdir = tempdir().unwrap();
1784 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1785
1786 let dir = tempdir.join("foo");
1787 std::fs::create_dir_all(&dir).unwrap();
1788 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1789 check(tempdir, input, dir);
1790 }
1791
1792 #[test]
1793 fn auto_cd_symlink() {
1794 let tempdir = tempdir().unwrap();
1795 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1796
1797 let dir = tempdir.join("foo");
1798 std::fs::create_dir_all(&dir).unwrap();
1799 let link = tempdir.join("link");
1800 symlink(&dir, &link).unwrap();
1801 let input = if cfg!(windows) { r".\link" } else { "./link" };
1802 check(tempdir, input, link);
1803
1804 let dir = tempdir.join("foo").join("bar");
1805 std::fs::create_dir_all(&dir).unwrap();
1806 let link = tempdir.join("link2");
1807 symlink(&dir, &link).unwrap();
1808 let input = "..";
1809 check(link, input, tempdir);
1810 }
1811
1812 #[test]
1813 #[should_panic(expected = "was not parsed into an auto-cd operation")]
1814 fn auto_cd_nonexistent_directory() {
1815 let tempdir = tempdir().unwrap();
1816 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1817
1818 let dir = tempdir.join("foo");
1819 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1820 check(tempdir, input, dir);
1821 }
1822
1823 #[test]
1824 fn escape_vscode_semicolon_test() {
1825 let input = r#"now;is"#;
1826 let expected = r#"now\x3Bis"#;
1827 let actual = escape_special_vscode_bytes(input).unwrap();
1828 assert_eq!(expected, actual);
1829 }
1830
1831 #[test]
1832 fn escape_vscode_backslash_test() {
1833 let input = r#"now\is"#;
1834 let expected = r#"now\\is"#;
1835 let actual = escape_special_vscode_bytes(input).unwrap();
1836 assert_eq!(expected, actual);
1837 }
1838
1839 #[test]
1840 fn escape_vscode_linefeed_test() {
1841 let input = "now\nis";
1842 let expected = r#"now\x0Ais"#;
1843 let actual = escape_special_vscode_bytes(input).unwrap();
1844 assert_eq!(expected, actual);
1845 }
1846
1847 #[test]
1848 fn escape_vscode_tab_null_cr_test() {
1849 let input = "now\t\0\ris";
1850 let expected = r#"now\x09\x00\x0Dis"#;
1851 let actual = escape_special_vscode_bytes(input).unwrap();
1852 assert_eq!(expected, actual);
1853 }
1854
1855 #[test]
1856 fn escape_vscode_multibyte_ok() {
1857 let input = "now🍪is";
1858 let actual = escape_special_vscode_bytes(input).unwrap();
1859 assert_eq!(input, actual);
1860 }
1861}