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