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 hints::ExternalHinter,
12 nu_highlight::NoOpHighlighter,
13 prompt_update,
14 reedline_config::{KeybindingsMode, add_menus, create_keybindings},
15 util::eval_source,
16};
17use crossterm::cursor::SetCursorStyle;
18use log::{error, trace, warn};
19use miette::{ErrReport, IntoDiagnostic, Result};
20use nu_cmd_base::util::get_editor;
21use nu_color_config::StyleComputer;
22#[allow(deprecated)]
23use nu_engine::env_to_strings;
24use nu_engine::exit::cleanup_exit;
25use nu_parser::{lex, parse, trim_quotes_str};
26use nu_protocol::shell_error::io::IoError;
27use nu_protocol::{BannerKind, shell_error};
28use nu_protocol::{
29 Config, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value,
30 config::NuCursorShape,
31 engine::{EngineState, Stack, StateWorkingSet},
32 report_shell_error,
33};
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, Instant},
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 "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 "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 {
282 to_type: "string".to_string(),
283 from_type: "bytes".to_string(),
284 span: Span::unknown(),
285 help: Some(format!(
286 "Error {err}, Unable to convert {input} to escaped bytes"
287 )),
288 })
289}
290
291fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
292 let mut start_time = std::time::Instant::now();
293 let mut line_editor = Reedline::create();
294
295 store_history_id_in_engine(engine_state, &line_editor);
297 perf!("setup reedline", start_time, use_color);
298
299 if let Some(history) = engine_state.history_config() {
300 start_time = std::time::Instant::now();
301
302 line_editor = setup_history(engine_state, line_editor, history)?;
303
304 perf!("setup history", start_time, use_color);
305 }
306 Ok(line_editor)
307}
308
309struct LoopContext<'a> {
310 engine_state: &'a mut EngineState,
311 stack: Stack,
312 line_editor: Reedline,
313 nu_prompt: &'a mut NushellPrompt,
314 temp_file: &'a Path,
315 use_color: bool,
316 entry_num: &'a mut usize,
317 hostname: Option<&'a str>,
318 mode_dispatcher: Option<std::sync::Arc<std::sync::Mutex<Box<dyn crate::ModeDispatcher>>>>,
319}
320
321#[inline]
324fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
325 use nu_cmd_base::hook;
326 use reedline::Signal;
327 let loop_start_time = std::time::Instant::now();
328
329 let LoopContext {
330 engine_state,
331 mut stack,
332 line_editor,
333 nu_prompt,
334 temp_file,
335 use_color,
336 entry_num,
337 hostname,
338 mut mode_dispatcher,
339 } = ctx;
340
341 let mut start_time = std::time::Instant::now();
342 if let Err(err) = engine_state.merge_env(&mut stack) {
345 report_shell_error(None, engine_state, &err);
346 }
347 perf!("merge env", start_time, use_color);
348
349 start_time = std::time::Instant::now();
350 engine_state.reset_signals();
351 perf!("reset signals", start_time, use_color);
352
353 start_time = std::time::Instant::now();
354 if let Err(error) = hook::eval_env_change_hook(
357 &engine_state.get_config().hooks.env_change.clone(),
358 engine_state,
359 &mut stack,
360 ) {
361 report_shell_error(None, engine_state, &error)
362 }
363 perf!("env-change hook", start_time, use_color);
364
365 start_time = std::time::Instant::now();
366 if let Err(err) = hook::eval_hooks(
368 engine_state,
369 &mut stack,
370 vec![],
371 &engine_state.get_config().hooks.pre_prompt.clone(),
372 "pre_prompt",
373 ) {
374 report_shell_error(None, engine_state, &err);
375 }
376 perf!("pre-prompt hook", start_time, use_color);
377
378 let engine_reference = Arc::new(engine_state.clone());
379 let config = stack.get_config(engine_state);
380
381 start_time = std::time::Instant::now();
382 let cursor_config = CursorConfig {
384 vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
385 vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
386 emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
387 };
388 perf!("get config/cursor config", start_time, use_color);
389
390 start_time = std::time::Instant::now();
391 let stack_arc = Arc::new(stack);
395 let term_program_is_vscode = engine_state
396 .get_env_var("TERM_PROGRAM")
397 .and_then(|v| v.as_str().ok())
398 == Some("vscode");
399 let mut line_editor = line_editor
400 .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
401 .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
404 ;
406 let shannon_mode = stack_arc.get_env_var(engine_state, "SHANNON_MODE")
407 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
408 .unwrap_or_else(|| "nu".to_string());
409 line_editor = if shannon_mode == "nu" {
410 line_editor.with_highlighter(Box::new(NuHighlighter {
411 engine_state: engine_reference.clone(),
412 stack: stack_arc.clone(),
414 }))
415 } else {
416 line_editor.with_highlighter(Box::<NoOpHighlighter>::default())
417 }
418 .with_validator(Box::new(NuValidator {
419 engine_state: engine_reference.clone(),
420 }))
421 .with_completer(Box::new(NuCompleter::new(
422 engine_reference.clone(),
423 stack_arc.clone(),
425 )))
426 .with_quick_completions(config.completions.quick)
427 .with_partial_completions(config.completions.partial)
428 .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
429 .with_cwd(Some(
430 engine_state
431 .cwd(None)
432 .map(|cwd| cwd.into_std_path_buf())
433 .unwrap_or_default()
434 .to_string_lossy()
435 .to_string(),
436 ))
437 .with_cursor_config(cursor_config)
438 .with_visual_selection_style(nu_ansi_term::Style {
439 is_reverse: true,
440 ..Default::default()
441 })
442 .with_semantic_markers(semantic_markers_from_config(
443 &config,
444 term_program_is_vscode,
445 ))
446 .with_mouse_click(if config.shell_integration.osc133 {
447 MouseClickMode::Enabled
448 } else {
449 MouseClickMode::Disabled
450 });
451
452 perf!("reedline builder", start_time, use_color);
453
454 let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
455
456 start_time = std::time::Instant::now();
457 line_editor = if config.use_ansi_coloring.get(engine_state) && config.show_hints {
458 let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
460 if let Some(closure) = config.hinter.closure.as_ref() {
461 line_editor.with_hinter(Box::new(ExternalHinter::new(
462 engine_reference.clone(),
463 stack_arc.clone(),
464 closure.clone(),
465 style,
466 )))
467 } else {
468 line_editor.with_hinter(Box::new(CwdAwareHinter::default().with_style(style)))
469 }
470 } else {
471 line_editor.disable_hints()
472 };
473
474 perf!("reedline coloring/style_computer", start_time, use_color);
475
476 start_time = std::time::Instant::now();
477 trace!("adding menus");
478 line_editor =
479 add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
480 report_shell_error(None, engine_state, &e);
481 Reedline::create()
482 });
483
484 perf!("reedline adding menus", start_time, use_color);
485
486 start_time = std::time::Instant::now();
487 let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
488
489 line_editor = if let Ok((cmd, args)) = buffer_editor {
490 let mut command = std::process::Command::new(cmd);
491 let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
492 warn!("Couldn't convert environment variable values to strings: {e}");
493 HashMap::default()
494 });
495 command.args(args).envs(envs);
496 line_editor.with_buffer_editor(command, temp_file.to_path_buf())
497 } else {
498 line_editor
499 };
500
501 perf!("reedline buffer_editor", start_time, use_color);
502
503 if let Some(history) = engine_state.history_config() {
504 start_time = std::time::Instant::now();
505 if history.sync_on_enter
506 && let Err(e) = line_editor.sync_history()
507 {
508 warn!("Failed to sync history: {e}");
509 }
510
511 perf!("sync_history", start_time, use_color);
512 }
513
514 start_time = std::time::Instant::now();
515 line_editor = setup_keybindings(engine_state, line_editor);
517
518 perf!("keybindings", start_time, use_color);
519
520 start_time = std::time::Instant::now();
521 let config = &engine_state.get_config().clone();
522 prompt_update::update_prompt(
523 config,
524 engine_state,
525 &mut Stack::with_parent(stack_arc.clone()),
526 nu_prompt,
527 );
528 let transient_prompt = prompt_update::make_transient_prompt(
529 config,
530 engine_state,
531 &mut Stack::with_parent(stack_arc.clone()),
532 nu_prompt,
533 );
534
535 perf!("update_prompt", start_time, use_color);
536
537 *entry_num += 1;
538
539 start_time = std::time::Instant::now();
540 line_editor = line_editor.with_transient_prompt(transient_prompt);
541 let input = line_editor.read_line(nu_prompt);
542 line_editor = line_editor
545 .with_highlighter(Box::<NoOpHighlighter>::default())
547 .with_completer(Box::<DefaultCompleter>::default())
549 .with_immediately_accept(false);
551
552 let shell_integration_osc2 = config.shell_integration.osc2;
554 let shell_integration_osc7 = config.shell_integration.osc7;
555 let shell_integration_osc9_9 = config.shell_integration.osc9_9;
556 let shell_integration_osc133 = config.shell_integration.osc133;
557 let shell_integration_osc633 = config.shell_integration.osc633;
558 let shell_integration_reset_application_mode = config.shell_integration.reset_application_mode;
559
560 let mut stack = Arc::unwrap_or_clone(stack_arc);
563
564 perf!("line_editor setup", start_time, use_color);
565
566 let line_editor_input_time = std::time::Instant::now();
567 match input {
568 Ok(Signal::Success(repl_cmd_line_text)) => {
569 if repl_cmd_line_text.trim() == "__shannon_switch" {
571 if let Some(ref _dispatcher) = mode_dispatcher {
572 let current = stack
573 .get_env_var(engine_state, "SHANNON_MODE")
574 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
575 .unwrap_or_else(|| "nu".to_string());
576 let modes = ["nu", "brush", "ai"];
577 let next_idx = modes.iter().position(|m| *m == current)
578 .map(|i| (i + 1) % modes.len())
579 .unwrap_or(0);
580 stack.add_env_var(
581 "SHANNON_MODE".to_string(),
582 Value::string(modes[next_idx], Span::unknown()),
583 );
584 }
585 return (true, stack, line_editor);
586 }
587 if repl_cmd_line_text.trim() == "exit" {
589 if let Some(ref _dispatcher) = mode_dispatcher {
590 let mode = stack
591 .get_env_var(engine_state, "SHANNON_MODE")
592 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
593 .unwrap_or_else(|| "nu".to_string());
594 if mode != "nu" {
595 return (false, stack, line_editor);
597 }
598 }
599 }
601
602 let history_supports_meta = match engine_state.history_config().map(|h| h.file_format) {
603 #[cfg(feature = "sqlite")]
604 Some(HistoryFileFormat::Sqlite) => true,
605 _ => false,
606 };
607
608 if history_supports_meta {
609 prepare_history_metadata(
610 &repl_cmd_line_text,
611 hostname,
612 engine_state,
613 &mut line_editor,
614 );
615 }
616
617 start_time = Instant::now();
619
620 {
623 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
625 repl.buffer = repl_cmd_line_text.to_string();
626 drop(repl);
627
628 if let Err(err) = hook::eval_hooks(
629 engine_state,
630 &mut stack,
631 vec![],
632 &engine_state.get_config().hooks.pre_execution.clone(),
633 "pre_execution",
634 ) {
635 report_shell_error(None, engine_state, &err);
636 }
637 }
638
639 perf!("pre_execution_hook", start_time, use_color);
640
641 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
642 repl.cursor_pos = line_editor.current_insertion_point();
643 repl.buffer = line_editor.current_buffer_contents().to_string();
644 drop(repl);
645
646 if shell_integration_osc633 {
647 if stack
648 .get_env_var(engine_state, "TERM_PROGRAM")
649 .and_then(|v| v.as_str().ok())
650 == Some("vscode")
651 {
652 start_time = Instant::now();
653
654 run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
655
656 perf!(
657 "pre_execute_marker (633;C) ansi escape sequence",
658 start_time,
659 use_color
660 );
661 } else if shell_integration_osc133 {
662 start_time = Instant::now();
663
664 run_ansi_sequence(PRE_EXECUTION_MARKER);
665
666 perf!(
667 "pre_execute_marker (133;C) ansi escape sequence",
668 start_time,
669 use_color
670 );
671 }
672 } else if shell_integration_osc133 {
673 start_time = Instant::now();
674
675 run_ansi_sequence(PRE_EXECUTION_MARKER);
676
677 perf!(
678 "pre_execute_marker (133;C) ansi escape sequence",
679 start_time,
680 use_color
681 );
682 }
683
684 let mut shannon_handled = false;
686 if let Some(ref dispatcher) = mode_dispatcher {
687 let mode = stack
688 .get_env_var(engine_state, "SHANNON_MODE")
689 .and_then(|v| v.as_str().ok().map(|s| s.to_string()))
690 .unwrap_or_else(|| "nu".to_string());
691 if mode != "nu" {
692 let env_strings = env_to_strings(engine_state, &stack).unwrap_or_default();
693 let cwd = stack
694 .get_env_var(engine_state, "PWD")
695 .and_then(|v| v.as_str().ok())
696 .map(std::path::PathBuf::from)
697 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
698 let result = dispatcher.lock().unwrap().execute(
699 &mode,
700 &repl_cmd_line_text,
701 env_strings,
702 cwd,
703 );
704 for (key, value) in &result.env {
706 stack.add_env_var(
707 key.clone(),
708 Value::string(value, Span::unknown()),
709 );
710 }
711 let _ = stack.set_cwd(&result.cwd);
712 let _ = std::env::set_current_dir(&result.cwd);
713 shannon_handled = true;
714 }
715 }
716
717 let cmd_execution_start_time = Instant::now();
719
720 if !shannon_handled {
721 match parse_operation(repl_cmd_line_text.clone(), engine_state, &stack) {
722 Ok(operation) => match operation {
723 ReplOperation::AutoCd { cwd, target, span } => {
724 do_auto_cd(target, cwd, &mut stack, engine_state, span);
725
726 run_finaliziation_ansi_sequence(
727 &stack,
728 engine_state,
729 use_color,
730 shell_integration_osc633,
731 shell_integration_osc133,
732 );
733 }
734 ReplOperation::RunCommand(cmd) => {
735 line_editor = do_run_cmd(
736 &cmd,
737 &mut stack,
738 engine_state,
739 line_editor,
740 shell_integration_osc2,
741 *entry_num,
742 use_color,
743 );
744
745 run_finaliziation_ansi_sequence(
746 &stack,
747 engine_state,
748 use_color,
749 shell_integration_osc633,
750 shell_integration_osc133,
751 );
752 }
753 ReplOperation::DoNothing => {}
755 },
756 Err(ref e) => error!("Error parsing operation: {e}"),
757 }
758 } let cmd_duration = cmd_execution_start_time.elapsed();
760
761 stack.add_env_var(
762 "CMD_DURATION_MS".into(),
763 Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
764 );
765
766 if history_supports_meta
767 && let Err(e) = fill_in_result_related_history_metadata(
768 &repl_cmd_line_text,
769 engine_state,
770 cmd_duration,
771 &mut stack,
772 &mut line_editor,
773 )
774 {
775 warn!("Could not fill in result related history metadata: {e}");
776 }
777
778 if shell_integration_osc2 {
779 run_shell_integration_osc2(None, engine_state, &mut stack, use_color);
780 }
781 if shell_integration_osc7 {
782 run_shell_integration_osc7(hostname, engine_state, &mut stack, use_color);
783 }
784 if shell_integration_osc9_9 {
785 run_shell_integration_osc9_9(engine_state, &mut stack, use_color);
786 }
787 if shell_integration_osc633 {
788 run_shell_integration_osc633(
789 engine_state,
790 &mut stack,
791 use_color,
792 repl_cmd_line_text,
793 );
794 }
795 if shell_integration_reset_application_mode {
796 run_shell_integration_reset_application_mode();
797 }
798
799 line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
800 }
801 Ok(Signal::CtrlC) => {
802 run_finaliziation_ansi_sequence(
804 &stack,
805 engine_state,
806 use_color,
807 shell_integration_osc633,
808 shell_integration_osc133,
809 );
810 }
811 Ok(Signal::CtrlD) => {
812 run_finaliziation_ansi_sequence(
815 &stack,
816 engine_state,
817 use_color,
818 shell_integration_osc633,
819 shell_integration_osc133,
820 );
821
822 println!();
823
824 cleanup_exit((), engine_state, 0);
825
826 return (true, stack, line_editor);
828 }
829 Ok(_) => {}
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 }
850 perf!(
851 "processing line editor input",
852 line_editor_input_time,
853 use_color
854 );
855
856 perf!(
857 "time between prompts in line editor loop",
858 loop_start_time,
859 use_color
860 );
861
862 (true, stack, line_editor)
863}
864
865fn prepare_history_metadata(
869 s: &str,
870 hostname: Option<&str>,
871 engine_state: &EngineState,
872 line_editor: &mut Reedline,
873) {
874 if !s.is_empty() && line_editor.has_last_command_context() {
875 let result = line_editor
876 .update_last_command_context(&|mut c| {
877 c.start_timestamp = Some(chrono::Utc::now());
878 c.hostname = hostname.map(str::to_string);
879 c.cwd = engine_state
880 .cwd(None)
881 .ok()
882 .map(|path| path.to_string_lossy().to_string());
883 c
884 })
885 .into_diagnostic();
886 if let Err(e) = result {
887 warn!("Could not prepare history metadata: {e}");
888 }
889 }
890}
891
892fn fill_in_result_related_history_metadata(
896 s: &str,
897 engine_state: &EngineState,
898 cmd_duration: Duration,
899 stack: &mut Stack,
900 line_editor: &mut Reedline,
901) -> Result<()> {
902 if !s.is_empty() && line_editor.has_last_command_context() {
903 line_editor
904 .update_last_command_context(&|mut c| {
905 c.duration = Some(cmd_duration);
906 c.exit_status = stack
907 .get_env_var(engine_state, "LAST_EXIT_CODE")
908 .and_then(|e| e.as_int().ok());
909 c
910 })
911 .into_diagnostic()?; }
913 Ok(())
914}
915
916enum ReplOperation {
918 AutoCd {
920 cwd: String,
922 target: PathBuf,
924 span: Span,
926 },
927 RunCommand(String),
929 DoNothing,
931}
932
933fn parse_operation(
941 s: String,
942 engine_state: &EngineState,
943 stack: &Stack,
944) -> Result<ReplOperation, ErrReport> {
945 let tokens = lex(s.as_bytes(), 0, &[], &[], false);
946 let cwd = engine_state
948 .cwd(Some(stack))
949 .map(|p| p.to_string_lossy().to_string())
950 .unwrap_or_default();
951 let mut orig = s.clone();
952 if orig.starts_with('`') {
953 orig = trim_quotes_str(&orig).to_string()
954 }
955
956 let path = nu_path::expand_path_with(&orig, &cwd, true);
957 if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 {
958 Ok(ReplOperation::AutoCd {
959 cwd,
960 target: path,
961 span: tokens.0[0].span,
962 })
963 } else if !s.trim().is_empty() {
964 Ok(ReplOperation::RunCommand(s))
965 } else {
966 Ok(ReplOperation::DoNothing)
967 }
968}
969
970fn do_auto_cd(
974 path: PathBuf,
975 cwd: String,
976 stack: &mut Stack,
977 engine_state: &mut EngineState,
978 span: Span,
979) {
980 let path = {
981 if !path.exists() {
982 report_shell_error(
983 Some(stack),
984 engine_state,
985 &ShellError::Io(IoError::new_with_additional_context(
986 shell_error::io::ErrorKind::DirectoryNotFound,
987 span,
988 PathBuf::from(&path),
989 "Cannot change directory",
990 )),
991 );
992 }
993 path.to_string_lossy().to_string()
994 };
995
996 if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
997 report_shell_error(
998 Some(stack),
999 engine_state,
1000 &ShellError::Io(IoError::new_with_additional_context(
1001 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
1002 span,
1003 PathBuf::from(path),
1004 "Cannot change directory",
1005 )),
1006 );
1007 return;
1008 }
1009
1010 stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown()));
1011
1012 if let Err(err) = stack.set_cwd(&path) {
1015 report_shell_error(Some(stack), engine_state, &err);
1016 return;
1017 };
1018 let cwd = Value::string(cwd, span);
1019
1020 let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
1021 let mut shells = if let Some(v) = shells {
1022 v.clone().into_list().unwrap_or_else(|_| vec![cwd])
1023 } else {
1024 vec![cwd]
1025 };
1026
1027 let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
1028 let current_shell = if let Some(v) = current_shell {
1029 v.as_int().unwrap_or_default() as usize
1030 } else {
1031 0
1032 };
1033
1034 let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
1035 let last_shell = if let Some(v) = last_shell {
1036 v.as_int().unwrap_or_default() as usize
1037 } else {
1038 0
1039 };
1040
1041 shells[current_shell] = Value::string(path, span);
1042
1043 stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
1044 stack.add_env_var(
1045 "NUSHELL_LAST_SHELL".into(),
1046 Value::int(last_shell as i64, span),
1047 );
1048 stack.set_last_exit_code(0, Span::unknown());
1049}
1050
1051fn do_run_cmd(
1056 s: &str,
1057 stack: &mut Stack,
1058 engine_state: &mut EngineState,
1059 line_editor: Reedline,
1062 shell_integration_osc2: bool,
1063 entry_num: usize,
1064 use_color: bool,
1065) -> Reedline {
1066 trace!("eval source: {s}");
1067
1068 let mut cmds = s.split_whitespace();
1069
1070 let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
1071
1072 if let Some("exit") = cmds.next() {
1073 let mut working_set = StateWorkingSet::new(engine_state);
1074 let _ = parse(&mut working_set, None, s.as_bytes(), false);
1075
1076 if working_set.parse_errors.is_empty() {
1077 match cmds.next() {
1078 Some(s) => {
1079 if let Ok(n) = s.parse::<i32>() {
1080 return cleanup_exit(line_editor, engine_state, n);
1081 }
1082 }
1083 None => {
1084 return cleanup_exit(line_editor, engine_state, 0);
1085 }
1086 }
1087 }
1088 }
1089
1090 if shell_integration_osc2 {
1091 run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
1092 }
1093
1094 eval_source(
1095 engine_state,
1096 stack,
1097 s.as_bytes(),
1098 &format!("repl_entry #{entry_num}"),
1099 PipelineData::empty(),
1100 false,
1101 );
1102
1103 if had_warning_before && engine_state.is_interactive {
1106 engine_state
1107 .exit_warning_given
1108 .store(false, Ordering::SeqCst);
1109 }
1110
1111 line_editor
1112}
1113
1114fn run_shell_integration_osc2(
1120 command_name: Option<&str>,
1121 engine_state: &EngineState,
1122 stack: &mut Stack,
1123 use_color: bool,
1124) {
1125 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1126 let start_time = Instant::now();
1127
1128 let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1130 let home_dir_str = p.as_path().display().to_string();
1131 if path.starts_with(&home_dir_str) {
1132 path.replacen(&home_dir_str, "~", 1)
1133 } else {
1134 path
1135 }
1136 } else {
1137 path
1138 };
1139
1140 let title = match command_name {
1141 Some(binary_name) => {
1142 let split_binary_name = binary_name.split_whitespace().next();
1143 if let Some(binary_name) = split_binary_name {
1144 format!("{maybe_abbrev_path}> {binary_name}")
1145 } else {
1146 maybe_abbrev_path.to_string()
1147 }
1148 }
1149 None => maybe_abbrev_path.to_string(),
1150 };
1151
1152 run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1158
1159 perf!("set title with command osc2", start_time, use_color);
1160 }
1161}
1162
1163fn run_shell_integration_osc7(
1164 hostname: Option<&str>,
1165 engine_state: &EngineState,
1166 stack: &mut Stack,
1167 use_color: bool,
1168) {
1169 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1170 let start_time = Instant::now();
1171
1172 let path = if cfg!(windows) {
1173 path.replace('\\', "/")
1174 } else {
1175 path
1176 };
1177
1178 run_ansi_sequence(&format!(
1180 "\x1b]7;file://{}{}{}\x1b\\",
1181 percent_encoding::utf8_percent_encode(
1182 hostname.unwrap_or("localhost"),
1183 percent_encoding::CONTROLS
1184 ),
1185 if path.starts_with('/') { "" } else { "/" },
1186 percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1187 ));
1188
1189 perf!(
1190 "communicate path to terminal with osc7",
1191 start_time,
1192 use_color
1193 );
1194 }
1195}
1196
1197fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1198 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1199 let start_time = Instant::now();
1200
1201 run_ansi_sequence(&format!("\x1b]9;9;{}\x1b\\", path));
1204
1205 perf!(
1206 "communicate path to terminal with osc9;9",
1207 start_time,
1208 use_color
1209 );
1210 }
1211}
1212
1213fn run_shell_integration_osc633(
1214 engine_state: &EngineState,
1215 stack: &mut Stack,
1216 use_color: bool,
1217 repl_cmd_line_text: String,
1218) {
1219 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1220 if stack
1223 .get_env_var(engine_state, "TERM_PROGRAM")
1224 .and_then(|v| v.as_str().ok())
1225 == Some("vscode")
1226 {
1227 let start_time = Instant::now();
1228
1229 run_ansi_sequence(&format!(
1232 "{VSCODE_CWD_PROPERTY_MARKER_PREFIX}{path}{VSCODE_CWD_PROPERTY_MARKER_SUFFIX}"
1233 ));
1234
1235 perf!(
1236 "communicate path to terminal with osc633;P",
1237 start_time,
1238 use_color
1239 );
1240
1241 let replaced_cmd_text =
1244 escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1245
1246 run_ansi_sequence(&format!(
1248 "{VSCODE_COMMANDLINE_MARKER_PREFIX}{replaced_cmd_text}{VSCODE_COMMANDLINE_MARKER_SUFFIX}"
1249 ));
1250 }
1251 }
1252}
1253
1254fn run_shell_integration_reset_application_mode() {
1255 run_ansi_sequence(RESET_APPLICATION_MODE);
1256}
1257
1258fn flush_engine_state_repl_buffer(
1262 engine_state: &mut EngineState,
1263 mut line_editor: Reedline,
1264) -> Reedline {
1265 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1266 line_editor.run_edit_commands(&[
1267 EditCommand::Clear,
1268 EditCommand::InsertString(repl.buffer.to_string()),
1269 EditCommand::MoveToPosition {
1270 position: repl.cursor_pos,
1271 select: false,
1272 },
1273 ]);
1274 if repl.accept {
1275 line_editor = line_editor.with_immediately_accept(true)
1276 }
1277 repl.accept = false;
1278 repl.buffer = "".to_string();
1279 repl.cursor_pos = 0;
1280 line_editor
1281}
1282
1283fn setup_history(
1287 engine_state: &mut EngineState,
1288 line_editor: Reedline,
1289 history: HistoryConfig,
1290) -> Result<Reedline> {
1291 let history_session_id = if history.isolation {
1293 Reedline::create_history_session_id()
1294 } else {
1295 None
1296 };
1297
1298 if let Some(path) = history.file_path() {
1299 return update_line_editor_history(
1300 engine_state,
1301 path,
1302 history,
1303 line_editor,
1304 history_session_id,
1305 );
1306 };
1307 Ok(line_editor)
1308}
1309
1310fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1314 match create_keybindings(engine_state.get_config()) {
1315 Ok(keybindings) => match keybindings {
1316 KeybindingsMode::Emacs(mut keybindings) => {
1317 keybindings.add_binding(
1319 crossterm::event::KeyModifiers::SHIFT,
1320 crossterm::event::KeyCode::BackTab,
1321 reedline::ReedlineEvent::ExecuteHostCommand("__shannon_switch".into()),
1322 );
1323 let edit_mode = Box::new(Emacs::new(keybindings));
1324 line_editor.with_edit_mode(edit_mode)
1325 }
1326 KeybindingsMode::Vi {
1327 mut insert_keybindings,
1328 mut normal_keybindings,
1329 } => {
1330 for kb in [&mut insert_keybindings, &mut normal_keybindings] {
1332 kb.add_binding(
1333 crossterm::event::KeyModifiers::SHIFT,
1334 crossterm::event::KeyCode::BackTab,
1335 reedline::ReedlineEvent::ExecuteHostCommand("__shannon_switch".into()),
1336 );
1337 }
1338 let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1339 line_editor.with_edit_mode(edit_mode)
1340 }
1341 },
1342 Err(e) => {
1343 report_shell_error(None, engine_state, &e);
1344 line_editor
1345 }
1346 }
1347}
1348
1349fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1353 if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1354 warn!("Terminal doesn't support use_kitty_protocol config");
1355 }
1356}
1357
1358fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1359 let session_id = line_editor
1360 .get_history_session_id()
1361 .map(i64::from)
1362 .unwrap_or(0);
1363
1364 engine_state.history_session_id = session_id;
1365}
1366
1367fn update_line_editor_history(
1368 engine_state: &mut EngineState,
1369 history_path: PathBuf,
1370 history: HistoryConfig,
1371 line_editor: Reedline,
1372 history_session_id: Option<HistorySessionId>,
1373) -> Result<Reedline, ErrReport> {
1374 let history: Box<dyn reedline::History> = match history.file_format {
1375 HistoryFileFormat::Plaintext => Box::new(
1376 FileBackedHistory::with_file(history.max_size as usize, history_path)
1377 .into_diagnostic()?,
1378 ),
1379 #[cfg(not(feature = "sqlite"))]
1381 HistoryFileFormat::Sqlite => {
1382 return Err(miette::miette!(
1383 help = "compile Nushell with the `sqlite` feature to use this",
1384 "Unsupported history file format",
1385 ));
1386 }
1387 #[cfg(feature = "sqlite")]
1388 HistoryFileFormat::Sqlite => Box::new(
1389 SqliteBackedHistory::with_file(
1390 history_path.to_path_buf(),
1391 history_session_id,
1392 Some(chrono::Utc::now()),
1393 )
1394 .into_diagnostic()?,
1395 ),
1396 };
1397 let line_editor = line_editor
1398 .with_history_session_id(history_session_id)
1399 .with_history_exclusion_prefix(Some(" ".into()))
1400 .with_history(history);
1401
1402 store_history_id_in_engine(engine_state, &line_editor);
1403
1404 Ok(line_editor)
1405}
1406
1407fn confirm_stdin_is_terminal() -> Result<()> {
1408 if !std::io::stdin().is_terminal() {
1411 return Err(std::io::Error::new(
1412 std::io::ErrorKind::NotFound,
1413 "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1414 ))
1415 .into_diagnostic();
1416 }
1417 Ok(())
1418}
1419fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1420 match shape {
1421 NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1422 NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1423 NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1424 NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1425 NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1426 NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1427 NuCursorShape::Inherit => None,
1428 }
1429}
1430
1431fn get_command_finished_marker(
1432 stack: &Stack,
1433 engine_state: &EngineState,
1434 shell_integration_osc633: bool,
1435 shell_integration_osc133: bool,
1436) -> String {
1437 let exit_code = stack
1438 .get_env_var(engine_state, "LAST_EXIT_CODE")
1439 .and_then(|e| e.as_int().ok());
1440
1441 if shell_integration_osc633 {
1442 if stack
1443 .get_env_var(engine_state, "TERM_PROGRAM")
1444 .and_then(|v| v.as_str().ok())
1445 == Some("vscode")
1446 {
1447 format!(
1449 "{}{}{}",
1450 VSCODE_POST_EXECUTION_MARKER_PREFIX,
1451 exit_code.unwrap_or(0),
1452 VSCODE_POST_EXECUTION_MARKER_SUFFIX
1453 )
1454 } else if shell_integration_osc133 {
1455 format!(
1457 "{}{}{}",
1458 POST_EXECUTION_MARKER_PREFIX,
1459 exit_code.unwrap_or(0),
1460 POST_EXECUTION_MARKER_SUFFIX
1461 )
1462 } else {
1463 "\x1b[0m".to_string()
1465 }
1466 } else if shell_integration_osc133 {
1467 format!(
1468 "{}{}{}",
1469 POST_EXECUTION_MARKER_PREFIX,
1470 exit_code.unwrap_or(0),
1471 POST_EXECUTION_MARKER_SUFFIX
1472 )
1473 } else {
1474 "\x1b[0m".to_string()
1475 }
1476}
1477
1478fn run_ansi_sequence(seq: &str) {
1479 if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1480 warn!("Error writing ansi sequence {e}");
1481 } else if let Err(e) = io::stdout().flush() {
1482 warn!("Error flushing stdio {e}");
1483 }
1484}
1485
1486fn run_finaliziation_ansi_sequence(
1487 stack: &Stack,
1488 engine_state: &EngineState,
1489 use_color: bool,
1490 shell_integration_osc633: bool,
1491 shell_integration_osc133: bool,
1492) {
1493 if shell_integration_osc633 {
1494 if stack
1496 .get_env_var(engine_state, "TERM_PROGRAM")
1497 .and_then(|v| v.as_str().ok())
1498 == Some("vscode")
1499 {
1500 let start_time = Instant::now();
1501
1502 run_ansi_sequence(&get_command_finished_marker(
1503 stack,
1504 engine_state,
1505 shell_integration_osc633,
1506 shell_integration_osc133,
1507 ));
1508
1509 perf!(
1510 "post_execute_marker (633;D) ansi escape sequences",
1511 start_time,
1512 use_color
1513 );
1514 } else if shell_integration_osc133 {
1515 let start_time = Instant::now();
1516
1517 run_ansi_sequence(&get_command_finished_marker(
1518 stack,
1519 engine_state,
1520 shell_integration_osc633,
1521 shell_integration_osc133,
1522 ));
1523
1524 perf!(
1525 "post_execute_marker (133;D) ansi escape sequences",
1526 start_time,
1527 use_color
1528 );
1529 }
1530 } else if shell_integration_osc133 {
1531 let start_time = Instant::now();
1532
1533 run_ansi_sequence(&get_command_finished_marker(
1534 stack,
1535 engine_state,
1536 shell_integration_osc633,
1537 shell_integration_osc133,
1538 ));
1539
1540 perf!(
1541 "post_execute_marker (133;D) ansi escape sequences",
1542 start_time,
1543 use_color
1544 );
1545 }
1546}
1547
1548#[cfg(windows)]
1550static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1551 fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1552});
1553
1554fn looks_like_path(orig: &str) -> bool {
1556 #[cfg(windows)]
1557 {
1558 if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1559 return true;
1560 }
1561 }
1562
1563 orig.starts_with('.')
1564 || orig.starts_with('~')
1565 || orig.starts_with('/')
1566 || orig.starts_with('\\')
1567 || orig.ends_with(std::path::MAIN_SEPARATOR)
1568}
1569
1570#[cfg(test)]
1571mod semantic_marker_tests {
1572 use super::semantic_markers_from_config;
1573 use nu_protocol::Config;
1574 use reedline::PromptKind;
1575
1576 #[test]
1577 fn semantic_markers_use_osc633_in_vscode() {
1578 let mut config = Config::default();
1579 config.shell_integration.osc633 = true;
1580 config.shell_integration.osc133 = true;
1581
1582 let markers =
1583 semantic_markers_from_config(&config, true).expect("expected semantic markers");
1584
1585 assert_eq!(
1586 markers.prompt_start(PromptKind::Primary).as_ref(),
1587 "\x1b]633;A;k=i\x1b\\"
1588 );
1589 }
1590
1591 #[test]
1592 fn semantic_markers_use_osc133_click_events() {
1593 let mut config = Config::default();
1594 config.shell_integration.osc133 = true;
1595
1596 let markers =
1597 semantic_markers_from_config(&config, false).expect("expected semantic markers");
1598
1599 assert_eq!(
1600 markers.prompt_start(PromptKind::Primary).as_ref(),
1601 "\x1b]133;A;k=i;click_events=1\x1b\\"
1602 );
1603 }
1604
1605 #[test]
1606 fn semantic_markers_none_when_disabled() {
1607 let mut config = Config::default();
1608 config.shell_integration.osc133 = false;
1609 config.shell_integration.osc633 = false;
1610 assert!(semantic_markers_from_config(&config, false).is_none());
1611 }
1612}
1613
1614#[cfg(windows)]
1615#[test]
1616fn looks_like_path_windows_drive_path_works() {
1617 assert!(looks_like_path("C:"));
1618 assert!(looks_like_path("D:\\"));
1619 assert!(looks_like_path("E:/"));
1620 assert!(looks_like_path("F:\\some_dir"));
1621 assert!(looks_like_path("G:/some_dir"));
1622}
1623
1624#[cfg(windows)]
1625#[test]
1626fn trailing_slash_looks_like_path() {
1627 assert!(looks_like_path("foo\\"))
1628}
1629
1630#[cfg(not(windows))]
1631#[test]
1632fn trailing_slash_looks_like_path() {
1633 assert!(looks_like_path("foo/"))
1634}
1635
1636#[test]
1637fn are_session_ids_in_sync() {
1638 let engine_state = &mut EngineState::new();
1639 let history = engine_state.history_config().unwrap();
1640 let history_path = history.file_path().unwrap();
1641 let line_editor = reedline::Reedline::create();
1642 let history_session_id = reedline::Reedline::create_history_session_id();
1643 let line_editor = update_line_editor_history(
1644 engine_state,
1645 history_path,
1646 history,
1647 line_editor,
1648 history_session_id,
1649 );
1650 assert_eq!(
1651 i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1652 engine_state.history_session_id
1653 );
1654}
1655
1656#[cfg(test)]
1657mod test_auto_cd {
1658 use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1659 use nu_path::AbsolutePath;
1660 use nu_protocol::engine::{EngineState, Stack};
1661 use tempfile::tempdir;
1662
1663 #[cfg(any(unix, windows))]
1665 fn symlink(
1666 original: impl AsRef<AbsolutePath>,
1667 link: impl AsRef<AbsolutePath>,
1668 ) -> std::io::Result<()> {
1669 let original = original.as_ref();
1670 let link = link.as_ref();
1671
1672 #[cfg(unix)]
1673 {
1674 std::os::unix::fs::symlink(original, link)
1675 }
1676 #[cfg(windows)]
1677 {
1678 if original.is_dir() {
1679 std::os::windows::fs::symlink_dir(original, link)
1680 } else {
1681 std::os::windows::fs::symlink_file(original, link)
1682 }
1683 }
1684 }
1685
1686 #[track_caller]
1690 fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1691 let mut engine_state = EngineState::new();
1693 let mut stack = Stack::new();
1694 stack.set_cwd(before.as_ref()).unwrap();
1695
1696 let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1698 let ReplOperation::AutoCd { cwd, target, span } = op else {
1699 panic!("'{input}' was not parsed into an auto-cd operation")
1700 };
1701
1702 do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1704 let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1705
1706 let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1710 let after = std::fs::canonicalize(after.as_ref()).unwrap();
1711 assert_eq!(updated_cwd, after);
1712 }
1713
1714 #[test]
1715 fn auto_cd_root() {
1716 let tempdir = tempdir().unwrap();
1717 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1718
1719 let input = if cfg!(windows) { r"C:\" } else { "/" };
1720 let root = AbsolutePath::try_new(input).unwrap();
1721 check(tempdir, input, root);
1722 }
1723
1724 #[test]
1725 fn auto_cd_tilde() {
1726 let tempdir = tempdir().unwrap();
1727 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1728
1729 let home = nu_path::home_dir().unwrap();
1730 check(tempdir, "~", home);
1731 }
1732
1733 #[test]
1734 fn auto_cd_dot() {
1735 let tempdir = tempdir().unwrap();
1736 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1737
1738 check(tempdir, ".", tempdir);
1739 }
1740
1741 #[test]
1742 fn auto_cd_double_dot() {
1743 let tempdir = tempdir().unwrap();
1744 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1745
1746 let dir = tempdir.join("foo");
1747 std::fs::create_dir_all(&dir).unwrap();
1748 check(dir, "..", tempdir);
1749 }
1750
1751 #[test]
1752 fn auto_cd_triple_dot() {
1753 let tempdir = tempdir().unwrap();
1754 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1755
1756 let dir = tempdir.join("foo").join("bar");
1757 std::fs::create_dir_all(&dir).unwrap();
1758 check(dir, "...", tempdir);
1759 }
1760
1761 #[test]
1762 fn auto_cd_relative() {
1763 let tempdir = tempdir().unwrap();
1764 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1765
1766 let foo = tempdir.join("foo");
1767 let bar = tempdir.join("bar");
1768 std::fs::create_dir_all(&foo).unwrap();
1769 std::fs::create_dir_all(&bar).unwrap();
1770 let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1771 check(foo, input, bar);
1772 }
1773
1774 #[test]
1775 fn auto_cd_trailing_slash() {
1776 let tempdir = tempdir().unwrap();
1777 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1778
1779 let dir = tempdir.join("foo");
1780 std::fs::create_dir_all(&dir).unwrap();
1781 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1782 check(tempdir, input, dir);
1783 }
1784
1785 #[test]
1786 fn auto_cd_symlink() {
1787 let tempdir = tempdir().unwrap();
1788 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1789
1790 let dir = tempdir.join("foo");
1791 std::fs::create_dir_all(&dir).unwrap();
1792 let link = tempdir.join("link");
1793 symlink(&dir, &link).unwrap();
1794 let input = if cfg!(windows) { r".\link" } else { "./link" };
1795 check(tempdir, input, link);
1796
1797 let dir = tempdir.join("foo").join("bar");
1798 std::fs::create_dir_all(&dir).unwrap();
1799 let link = tempdir.join("link2");
1800 symlink(&dir, &link).unwrap();
1801 let input = "..";
1802 check(link, input, tempdir);
1803 }
1804
1805 #[test]
1806 #[should_panic(expected = "was not parsed into an auto-cd operation")]
1807 fn auto_cd_nonexistent_directory() {
1808 let tempdir = tempdir().unwrap();
1809 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1810
1811 let dir = tempdir.join("foo");
1812 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1813 check(tempdir, input, dir);
1814 }
1815
1816 #[test]
1817 fn escape_vscode_semicolon_test() {
1818 let input = "now;is";
1819 let expected = r#"now\x3Bis"#;
1820 let actual = escape_special_vscode_bytes(input).unwrap();
1821 assert_eq!(expected, actual);
1822 }
1823
1824 #[test]
1825 fn escape_vscode_backslash_test() {
1826 let input = r#"now\is"#;
1827 let expected = r#"now\\is"#;
1828 let actual = escape_special_vscode_bytes(input).unwrap();
1829 assert_eq!(expected, actual);
1830 }
1831
1832 #[test]
1833 fn escape_vscode_linefeed_test() {
1834 let input = "now\nis";
1835 let expected = r#"now\x0Ais"#;
1836 let actual = escape_special_vscode_bytes(input).unwrap();
1837 assert_eq!(expected, actual);
1838 }
1839
1840 #[test]
1841 fn escape_vscode_tab_null_cr_test() {
1842 let input = "now\t\0\ris";
1843 let expected = r#"now\x09\x00\x0Dis"#;
1844 let actual = escape_special_vscode_bytes(input).unwrap();
1845 assert_eq!(expected, actual);
1846 }
1847
1848 #[test]
1849 fn escape_vscode_multibyte_ok() {
1850 let input = "now🍪is";
1851 let actual = escape_special_vscode_bytes(input).unwrap();
1852 assert_eq!(input, actual);
1853 }
1854}