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