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::{NarrowingCache, NuCompleter},
11 hints::ExternalHinter,
12 prompt_update,
13 reedline_config::{KeybindingsMode, add_menus, create_keybindings},
14 syntax_highlight::NoOpHighlighter,
15 util::{eval_source, evaluate_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;
22use nu_engine::env_to_strings;
23use nu_engine::exit::cleanup_exit;
24use nu_parser::{lex, trim_quotes_str};
25use nu_protocol::shell_error::io::IoError;
26use nu_protocol::{BannerKind, ShellIntegrationConfig, shell_error};
27use nu_protocol::{
28 Config, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value,
29 config::NuCursorShape,
30 engine::{EngineState, Stack},
31 report_shell_error,
32};
33use nu_utils::time::Instant;
34use nu_utils::{
35 filesystem::{PermissionResult, have_permission},
36 perf, stderr_write_all_and_flush, stdout_write_all_and_flush,
37};
38#[cfg(feature = "helix")]
39use reedline::Helix;
40#[cfg(feature = "sqlite")]
41use reedline::SqliteBackedHistory;
42use reedline::{
43 CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
44 HistorySessionId, MouseClickMode, Osc133ClickEventsMarkers, Osc633Markers, Reedline,
45 SemanticPromptMarkers, Vi,
46};
47use std::sync::atomic::Ordering;
48use std::{
49 collections::HashMap,
50 env::temp_dir,
51 io::{self, IsTerminal, Write},
52 panic::{AssertUnwindSafe, catch_unwind},
53 path::{Path, PathBuf},
54 sync::Arc,
55 time::Duration,
56};
57use sysinfo::System;
58
59fn semantic_markers_from_config(
60 config: &Config,
61 term_program_is_vscode: bool,
62) -> Option<Box<dyn SemanticPromptMarkers>> {
63 if config.shell_integration.osc633 && term_program_is_vscode {
64 Some(Osc633Markers::boxed())
65 } else if config.shell_integration.osc133 {
66 Some(Osc133ClickEventsMarkers::boxed())
67 } else {
68 None
69 }
70}
71
72type RepaintCallback = Arc<dyn Fn() + Send + Sync>;
73
74fn build_interactive_prompt(
77 engine_state: &EngineState,
78 line_editor: &mut Reedline,
79) -> NushellPrompt {
80 let jobs_active = {
84 let active_jobs = engine_state
85 .jobs
86 .lock()
87 .unwrap_or_else(|poisoned| poisoned.into_inner());
88 !active_jobs.is_empty()
89 };
90
91 let background_repainter = jobs_active.then(|| {
92 let repaint_signal = line_editor.repaint_signal();
93 Arc::new(move || repaint_signal.request_repaint()) as RepaintCallback
94 });
95
96 engine_state
97 .prompt_state
98 .set_repainter(background_repainter);
99
100 NushellPrompt::shared(engine_state.prompt_state.clone())
101}
102
103pub fn evaluate_repl(
105 engine_state: &mut EngineState,
106 stack: Stack,
107 prerun_command: Option<Spanned<String>>,
108 load_std_lib: Option<Spanned<String>>,
109 entire_start_time: Instant,
110) -> Result<()> {
111 let mut unique_stack = stack.clone();
117 let config = engine_state.get_config();
118 let use_color = config.use_ansi_coloring.get(engine_state);
119
120 let mut entry_num = 0;
121 let mut is_hostcommand = false;
122
123 let shell_integration_osc2 = config.shell_integration.osc2;
125 let shell_integration_osc7 = config.shell_integration.osc7;
126 let shell_integration_osc9_9 = config.shell_integration.osc9_9;
127 let shell_integration_osc633 = config.shell_integration.osc633;
128
129 unique_stack.add_env_var(
131 "CMD_DURATION_MS".into(),
132 Value::string("0823", Span::unknown()),
133 );
134
135 unique_stack.set_last_exit_code(0, Span::unknown());
136
137 let mut line_editor = get_line_editor(engine_state, use_color)?;
138 let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
139
140 if let Some(s) = prerun_command {
141 eval_source(
142 engine_state,
143 &mut unique_stack,
144 s.item.as_bytes(),
145 &format!("repl_entry #{entry_num}"),
146 PipelineData::empty(),
147 false,
148 );
149 engine_state.merge_env(&mut unique_stack)?;
150 }
151
152 confirm_stdin_is_terminal()?;
153
154 let hostname = System::host_name();
155 if shell_integration_osc2 {
156 run_shell_integration_osc2(None, engine_state, &mut unique_stack, use_color);
157 }
158 if shell_integration_osc7 {
159 run_shell_integration_osc7(
160 hostname.as_deref(),
161 engine_state,
162 &mut unique_stack,
163 use_color,
164 );
165 }
166 if shell_integration_osc9_9 {
167 run_shell_integration_osc9_9(engine_state, &mut unique_stack, use_color);
168 }
169 if shell_integration_osc633 {
170 let cmd_text = line_editor.current_buffer_contents().to_string();
173
174 let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
175
176 run_shell_integration_osc633(
177 engine_state,
178 &mut unique_stack,
179 use_color,
180 replaced_cmd_text,
181 );
182 }
183
184 engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64);
185
186 engine_state.generate_nu_constant();
188
189 if load_std_lib.is_none() {
190 match engine_state.get_config().show_banner {
191 BannerKind::None => {}
192 BannerKind::Short => {
193 eval_source(
194 engine_state,
195 &mut unique_stack,
196 "banner --short".as_bytes(),
197 "show short banner",
198 PipelineData::empty(),
199 false,
200 );
201 }
202 BannerKind::Full => {
203 eval_source(
204 engine_state,
205 &mut unique_stack,
206 "banner".as_bytes(),
207 "show_banner",
208 PipelineData::empty(),
209 false,
210 );
211 }
212 }
213 }
214
215 kitty_protocol_healthcheck(engine_state);
216
217 let mut previous_engine_state = engine_state.clone();
219 let mut previous_stack_arc = Arc::new(unique_stack);
220 let completion_cache = NarrowingCache::default();
222 loop {
223 let mut current_engine_state = previous_engine_state.clone();
227 let current_stack = Stack::with_parent(previous_stack_arc.clone());
230 let temp_file_cloned = temp_file.clone();
231 let current_completion_cache = completion_cache.clone();
232
233 let iteration_panic_state = catch_unwind(AssertUnwindSafe(|| {
234 let (continue_loop, current_stack, line_editor) = loop_iteration(LoopContext {
235 engine_state: &mut current_engine_state,
236 stack: current_stack,
237 line_editor,
238 temp_file: &temp_file_cloned,
239 use_color,
240 entry_num: &mut entry_num,
241 hostname: hostname.as_deref(),
242 is_hostcommand: &mut is_hostcommand,
243 completion_cache: current_completion_cache,
244 });
245
246 (
248 continue_loop,
249 current_engine_state,
250 current_stack,
251 line_editor,
252 )
253 }));
254 match iteration_panic_state {
255 Ok((continue_loop, mut es, s, le)) => {
256 let mut merged_stack = Stack::with_changes_from_child(previous_stack_arc, s);
258
259 let prev_total_vars = previous_engine_state.num_vars();
261 let curr_total_vars = es.num_vars();
262 let new_variables_created = curr_total_vars > prev_total_vars;
263
264 if new_variables_created {
265 es.cleanup_stack_variables(&mut merged_stack);
267 }
268
269 previous_stack_arc = Arc::new(merged_stack);
270 previous_engine_state = es;
272 line_editor = le;
273 if !continue_loop {
274 break;
275 }
276 }
277 Err(_) => {
278 line_editor = get_line_editor(engine_state, use_color)?;
280 }
281 }
282 }
283
284 Ok(())
285}
286
287fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
288 let bytes = input
289 .chars()
290 .flat_map(|c| {
291 let mut buf = [0; 4]; let c_bytes = c.encode_utf8(&mut buf); if c_bytes.len() == 1 {
295 let byte = c_bytes.as_bytes()[0];
296
297 match byte {
298 b if b < 0x20 => format!("\\x{byte:02X}").into_bytes(),
300 b';' => "\\x3B".to_string().into_bytes(),
302 b'\\' => "\\\\".to_string().into_bytes(),
304 _ => vec![byte],
306 }
307 } else {
308 c_bytes.bytes().collect()
310 }
311 })
312 .collect();
313
314 String::from_utf8(bytes).map_err(|err| ShellError::CantConvert {
316 to_type: "string".to_string(),
317 from_type: "bytes".to_string(),
318 span: Span::unknown(),
319 help: Some(format!(
320 "Error {err}, Unable to convert {input} to escaped bytes"
321 )),
322 })
323}
324
325fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
326 let mut start_time = Instant::now();
327 let mut line_editor = Reedline::create();
328
329 store_history_id_in_engine(engine_state, &line_editor);
331 perf!("setup reedline", start_time, use_color);
332
333 if let Some(history) = engine_state.history_config() {
334 start_time = Instant::now();
335
336 line_editor = setup_history(engine_state, line_editor, history)?;
337
338 engine_state.history_locked_after_startup = true;
343
344 perf!("setup history", start_time, use_color);
345 }
346 Ok(line_editor)
347}
348
349struct LoopContext<'a> {
350 engine_state: &'a mut EngineState,
351 stack: Stack,
352 line_editor: Reedline,
353 temp_file: &'a Path,
354 use_color: bool,
355 entry_num: &'a mut usize,
356 hostname: Option<&'a str>,
357 is_hostcommand: &'a mut bool,
358 completion_cache: NarrowingCache,
360}
361
362struct RunContext<'a> {
363 engine_state: &'a mut EngineState,
364 stack: &'a mut Stack,
365 line_editor: Reedline,
366 command: String,
367 hostname: Option<&'a str>,
368 use_color: bool,
369 shell_integration: &'a ShellIntegrationConfig,
370 entry_num: &'a mut usize,
371}
372
373fn run_command(ctx: RunContext) -> Reedline {
374 use nu_cmd_base::hook;
375
376 let RunContext {
377 engine_state,
378 stack,
379 mut line_editor,
380 command,
381 hostname,
382 use_color,
383 shell_integration,
384 entry_num,
385 } = ctx;
386
387 let history_supports_meta = match engine_state.history_config().map(|h| h.file_format) {
388 #[cfg(feature = "sqlite")]
389 Some(HistoryFileFormat::Sqlite) => true,
390 _ => false,
391 };
392
393 if history_supports_meta {
394 prepare_history_metadata(&command, hostname, engine_state, &mut line_editor);
395 }
396
397 let mut start_time = Instant::now();
399
400 {
403 if let Err(err) = hook::eval_pre_execution_hooks(engine_state, stack, command.clone()) {
404 report_shell_error(None, engine_state, &err);
405 }
406 }
407
408 perf!("pre_execution_hook", start_time, use_color);
409
410 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
411 repl.cursor_pos = line_editor.current_insertion_point();
412 repl.buffer = line_editor.current_buffer_contents().to_string();
413 drop(repl);
414
415 if shell_integration.osc633 {
416 if stack
417 .get_env_var(engine_state, "TERM_PROGRAM")
418 .and_then(|v| v.as_str().ok())
419 == Some("vscode")
420 {
421 start_time = Instant::now();
422
423 run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
424
425 perf!(
426 "pre_execute_marker (633;C) ansi escape sequence",
427 start_time,
428 use_color
429 );
430 } else if shell_integration.osc133 {
431 start_time = Instant::now();
432
433 run_ansi_sequence(PRE_EXECUTION_MARKER);
434
435 perf!(
436 "pre_execute_marker (133;C) ansi escape sequence",
437 start_time,
438 use_color
439 );
440 }
441 } else if shell_integration.osc133 {
442 start_time = Instant::now();
443
444 run_ansi_sequence(PRE_EXECUTION_MARKER);
445
446 perf!(
447 "pre_execute_marker (133;C) ansi escape sequence",
448 start_time,
449 use_color
450 );
451 }
452
453 let cmd_execution_start_time = Instant::now();
455
456 let mut snapshot_ans = false;
458
459 match parse_operation(command.clone(), engine_state, stack) {
460 Ok(ReplOperation::AutoCd { cwd, target, span }) => {
461 do_auto_cd(target, cwd, stack, engine_state, span);
462
463 run_finaliziation_ansi_sequence(
464 stack,
465 engine_state,
466 use_color,
467 shell_integration.osc633,
468 shell_integration.osc133,
469 );
470 }
471 Ok(ReplOperation::RunCommand(cmd)) => {
472 line_editor = do_run_cmd(
473 &cmd,
474 stack,
475 engine_state,
476 line_editor,
477 shell_integration.osc2,
478 *entry_num,
479 use_color,
480 );
481
482 run_finaliziation_ansi_sequence(
483 stack,
484 engine_state,
485 use_color,
486 shell_integration.osc633,
487 shell_integration.osc133,
488 );
489 snapshot_ans = true;
490 }
491 Ok(ReplOperation::DoNothing) => {}
493 Err(ref e) => error!("Error parsing operation: {e}"),
494 }
495 let cmd_duration = cmd_execution_start_time.elapsed();
496
497 stack.add_env_var(
499 "CMD_DURATION_MS".into(),
500 Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
501 );
502
503 if snapshot_ans {
508 stack.snapshot_ans_repl_metadata(engine_state, cmd_duration, &command);
509 }
510
511 if history_supports_meta
512 && let Err(e) = fill_in_result_related_history_metadata(
513 &command,
514 engine_state,
515 cmd_duration,
516 stack,
517 &mut line_editor,
518 )
519 {
520 warn!("Could not fill in result related history metadata: {e}");
521 }
522
523 if shell_integration.osc2 {
524 run_shell_integration_osc2(None, engine_state, stack, use_color);
525 }
526 if shell_integration.osc7 {
527 run_shell_integration_osc7(hostname, engine_state, stack, use_color);
528 }
529 if shell_integration.osc9_9 {
530 run_shell_integration_osc9_9(engine_state, stack, use_color);
531 }
532 if shell_integration.osc633 {
533 run_shell_integration_osc633(engine_state, stack, use_color, command);
534 }
535 if shell_integration.reset_application_mode {
536 run_shell_integration_reset_application_mode();
537 }
538
539 line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
540 line_editor
541}
542
543#[inline]
546fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
547 use nu_cmd_base::hook;
548 use reedline::Signal;
549 let loop_start_time = Instant::now();
550
551 let LoopContext {
552 engine_state,
553 mut stack,
554 line_editor,
555 temp_file,
556 use_color,
557 entry_num,
558 hostname,
559 is_hostcommand,
560 completion_cache,
561 } = ctx;
562
563 let mut start_time = Instant::now();
564 if let Err(err) = engine_state.merge_env(&mut stack) {
567 report_shell_error(None, engine_state, &err);
568 }
569 perf!("merge env", start_time, use_color);
570
571 start_time = Instant::now();
572 engine_state.reset_signals();
573 perf!("reset signals", start_time, use_color);
574
575 start_time = Instant::now();
576
577 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
579 repl.cursor_pos = line_editor.current_insertion_point();
580 repl.buffer = line_editor.current_buffer_contents().to_string();
581 drop(repl);
582
583 if let Err(error) = hook::eval_env_change_hook(
586 &engine_state.get_config().hooks.env_change.clone(),
587 engine_state,
588 &mut stack,
589 ) {
590 report_shell_error(None, engine_state, &error)
591 }
592 perf!("env-change hook", start_time, use_color);
593
594 start_time = Instant::now();
595 if let Err(err) = hook::eval_pre_prompt_hooks(engine_state, &mut stack) {
597 report_shell_error(None, engine_state, &err);
598 }
599 perf!("pre-prompt hook", start_time, use_color);
600
601 let engine_reference = Arc::new(engine_state.clone());
602 let config = stack.get_config(engine_state);
603
604 start_time = Instant::now();
605 let cursor_config = CursorConfig {
607 vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
608 vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
609 emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
610 #[cfg(feature = "helix")]
611 hx_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.helix_insert),
612 #[cfg(feature = "helix")]
613 hx_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.helix_normal),
614 #[cfg(feature = "helix")]
615 hx_select: map_nucursorshape_to_cursorshape(config.cursor_shape.helix_select),
616 };
617 perf!("get config/cursor config", start_time, use_color);
618
619 start_time = Instant::now();
620 let stack_arc = Arc::new(stack);
624 let term_program_is_vscode = engine_state
625 .get_env_var("TERM_PROGRAM")
626 .and_then(|v| v.as_str().ok())
627 == Some("vscode");
628 let mut line_editor = line_editor
629 .use_kitty_keyboard_enhancement(config.use_kitty_protocol)
630 .use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
633 .with_highlighter(Box::new(NuHighlighter::new(
634 engine_reference.clone(),
635 stack_arc.clone(),
637 )))
638 .with_validator(Box::new(NuValidator {
639 engine_state: engine_reference.clone(),
640 }))
641 .with_completer(Box::new(NuCompleter::for_repl(
642 engine_reference.clone(),
643 stack_arc.clone(),
645 completion_cache,
646 )))
647 .with_quick_completions(config.completions.quick)
648 .with_partial_completions(config.completions.partial)
649 .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
650 .with_cwd(Some(
651 engine_state
652 .cwd(None)
653 .map(|cwd| cwd.into_std_path_buf())
654 .unwrap_or_default()
655 .to_string_lossy()
656 .to_string(),
657 ))
658 .with_cursor_config(cursor_config)
659 .with_abbreviations(config.abbreviations.clone())
660 .with_semantic_markers(semantic_markers_from_config(
661 &config,
662 term_program_is_vscode,
663 ))
664 .with_mouse_click(if config.shell_integration.osc133 {
665 MouseClickMode::Enabled
666 } else {
667 MouseClickMode::Disabled
668 });
669
670 perf!("reedline builder", start_time, use_color);
671
672 let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
673
674 start_time = Instant::now();
675 line_editor = line_editor.with_visual_selection_style(
678 style_computer.compute("selection", &Value::nothing(Span::unknown())),
679 );
680 line_editor = line_editor.with_visual_selection_cursor_style(
681 style_computer.compute("selection_cursor", &Value::nothing(Span::unknown())),
682 );
683 line_editor = if config.use_ansi_coloring.get(engine_state) && config.show_hints {
684 let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
687 if let Some(closure) = config.hinter.closure.as_ref() {
688 line_editor.with_hinter(Box::new(ExternalHinter::new(
689 engine_reference.clone(),
690 stack_arc.clone(),
691 closure.clone(),
692 style,
693 )))
694 } else {
695 line_editor.with_hinter(Box::new(CwdAwareHinter::default().with_style(style)))
696 }
697 } else {
698 line_editor.disable_hints()
699 };
700
701 perf!("reedline coloring/style_computer", start_time, use_color);
702
703 start_time = Instant::now();
704 trace!("adding menus");
705 line_editor =
706 add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
707 report_shell_error(None, engine_state, &e);
708 Reedline::create()
709 });
710
711 perf!("reedline adding menus", start_time, use_color);
712
713 start_time = Instant::now();
714 let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
716
717 line_editor = if let Ok((cmd, args)) = buffer_editor {
718 let mut command = std::process::Command::new(cmd);
719 let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
720 warn!("Couldn't convert environment variable values to strings: {e}");
721 HashMap::default()
722 });
723 command.args(args).envs(envs);
724 line_editor.with_buffer_editor(command, temp_file.to_path_buf())
725 } else {
726 line_editor
727 };
728
729 perf!("reedline buffer_editor", start_time, use_color);
730
731 if let Some(history) = engine_state.history_config() {
732 start_time = Instant::now();
733
734 line_editor = line_editor
735 .with_history_exclusion_prefix(history.ignore_space_prefixed.then_some(" ".into()));
736
737 if history.sync_on_enter
738 && let Err(e) = line_editor.sync_history()
739 {
740 warn!("Failed to sync history: {e}");
741 }
742
743 perf!("sync_history", start_time, use_color);
744 }
745
746 start_time = Instant::now();
747 line_editor = setup_keybindings(engine_state, line_editor);
749
750 perf!("keybindings", start_time, use_color);
751
752 start_time = Instant::now();
753 let config = &engine_state.get_config().clone();
754 prompt_update::update_prompt(
757 config,
758 engine_state,
759 &mut Stack::with_parent(stack_arc.clone()),
760 );
761 let transient_prompt = prompt_update::make_transient_prompt(
762 config,
763 engine_state,
764 &mut Stack::with_parent(stack_arc.clone()),
765 );
766
767 perf!("update_prompt", start_time, use_color);
768
769 if !*is_hostcommand {
774 line_editor = flush_engine_state_repl_buffer(engine_state, line_editor);
775 }
776 *is_hostcommand = false;
777
778 *entry_num += 1;
779
780 start_time = Instant::now();
781 line_editor = line_editor.with_transient_prompt(transient_prompt);
782
783 let interactive_prompt = build_interactive_prompt(engine_state, &mut line_editor);
784 let input = line_editor.read_line(&interactive_prompt);
785
786 line_editor = line_editor
788 .with_highlighter(Box::<NoOpHighlighter>::default())
790 .with_completer(Box::<DefaultCompleter>::default())
792 .with_immediately_accept(false);
794
795 let shell_integration = &config.shell_integration;
796
797 let mut stack = Arc::unwrap_or_clone(stack_arc);
800
801 perf!("line_editor setup", start_time, use_color);
802
803 let line_editor_input_time = Instant::now();
804 match input {
805 Ok(Signal::Success(command)) => {
806 line_editor = run_command(RunContext {
807 engine_state,
808 stack: &mut stack,
809 line_editor,
810 command,
811 hostname,
812 use_color,
813 shell_integration,
814 entry_num,
815 });
816 }
817 Ok(Signal::HostCommand(command)) => {
818 *is_hostcommand = true;
819 line_editor = run_command(RunContext {
820 engine_state,
821 stack: &mut stack,
822 line_editor,
823 command,
824 hostname,
825 use_color,
826 shell_integration,
827 entry_num,
828 });
829 }
830 Ok(Signal::CtrlC) => {
831 run_finaliziation_ansi_sequence(
833 &stack,
834 engine_state,
835 use_color,
836 shell_integration.osc633,
837 shell_integration.osc133,
838 );
839 }
840 Ok(Signal::CtrlD) => {
841 run_finaliziation_ansi_sequence(
844 &stack,
845 engine_state,
846 use_color,
847 shell_integration.osc633,
848 shell_integration.osc133,
849 );
850
851 let _ = stdout_write_all_and_flush("\n");
853
854 cleanup_exit((), engine_state, 0);
855
856 return (true, stack, line_editor);
858 }
859 Ok(_) => {}
861 Err(err) => {
862 if !err.to_string().contains("duration") {
863 write_repl_error_details(&err);
864 cleanup_exit((), engine_state, 1);
865 return (true, stack, line_editor);
866 }
867
868 run_finaliziation_ansi_sequence(
869 &stack,
870 engine_state,
871 use_color,
872 shell_integration.osc633,
873 shell_integration.osc133,
874 );
875 }
876 }
877 perf!(
878 "processing line editor input",
879 line_editor_input_time,
880 use_color
881 );
882
883 perf!(
884 "time between prompts in line editor loop",
885 loop_start_time,
886 use_color
887 );
888
889 (true, stack, line_editor)
890}
891
892fn prepare_history_metadata(
896 s: &str,
897 hostname: Option<&str>,
898 engine_state: &EngineState,
899 line_editor: &mut Reedline,
900) {
901 if !s.is_empty() && line_editor.has_last_command_context() {
902 let result = line_editor
903 .update_last_command_context(&|mut c| {
904 c.start_timestamp = Some(chrono::Utc::now());
905 c.hostname = hostname.map(str::to_string);
906 c.cwd = engine_state
907 .cwd(None)
908 .ok()
909 .map(|path| path.to_string_lossy().to_string());
910 c
911 })
912 .into_diagnostic();
913 if let Err(e) = result {
914 warn!("Could not prepare history metadata: {e}");
915 }
916 }
917}
918
919fn fill_in_result_related_history_metadata(
923 s: &str,
924 engine_state: &EngineState,
925 cmd_duration: Duration,
926 stack: &mut Stack,
927 line_editor: &mut Reedline,
928) -> Result<()> {
929 if !s.is_empty() && line_editor.has_last_command_context() {
930 line_editor
931 .update_last_command_context(&|mut c| {
932 c.duration = Some(cmd_duration);
933 c.exit_status = stack
934 .get_env_var(engine_state, "LAST_EXIT_CODE")
935 .and_then(|e| e.as_int().ok());
936 c
937 })
938 .into_diagnostic()?; }
940 Ok(())
941}
942
943enum ReplOperation {
945 AutoCd {
947 cwd: String,
949 target: PathBuf,
951 span: Span,
953 },
954 RunCommand(String),
956 DoNothing,
958}
959
960fn parse_operation(
968 s: String,
969 engine_state: &EngineState,
970 stack: &Stack,
971) -> Result<ReplOperation, ErrReport> {
972 let tokens = lex(s.as_bytes(), 0, &[], &[], false);
973 let cwd = engine_state
975 .cwd(Some(stack))
976 .map(|p| p.to_string_lossy().to_string())
977 .unwrap_or_default();
978 let mut orig = s.trim().to_string();
979 if orig.starts_with('`') {
980 orig = trim_quotes_str(&orig).to_string()
981 }
982
983 let path = nu_path::expand_path_with(&orig, &cwd, true);
984 if (engine_state.get_config().auto_cd_implicit || looks_like_path(&orig))
985 && path.is_dir()
986 && tokens.0.len() == 1
987 {
988 Ok(ReplOperation::AutoCd {
989 cwd,
990 target: path,
991 span: tokens.0[0].span,
992 })
993 } else if !s.trim().is_empty() {
994 Ok(ReplOperation::RunCommand(s))
995 } else {
996 Ok(ReplOperation::DoNothing)
997 }
998}
999
1000fn do_auto_cd(
1004 path: PathBuf,
1005 cwd: String,
1006 stack: &mut Stack,
1007 engine_state: &mut EngineState,
1008 span: Span,
1009) {
1010 let path = {
1011 if !path.exists() {
1012 report_shell_error(
1013 Some(stack),
1014 engine_state,
1015 &ShellError::Io(IoError::new_with_additional_context(
1016 shell_error::io::ErrorKind::DirectoryNotFound,
1017 span,
1018 PathBuf::from(&path),
1019 "Cannot change directory",
1020 )),
1021 );
1022 }
1023 path.to_string_lossy().to_string()
1024 };
1025
1026 if let PermissionResult::PermissionDenied = have_permission(path.clone()) {
1027 report_shell_error(
1028 Some(stack),
1029 engine_state,
1030 &ShellError::Io(IoError::new_with_additional_context(
1031 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::PermissionDenied),
1032 span,
1033 PathBuf::from(path),
1034 "Cannot change directory",
1035 )),
1036 );
1037 return;
1038 }
1039
1040 stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), span));
1041
1042 if let Err(err) = stack.set_cwd(&path) {
1045 report_shell_error(Some(stack), engine_state, &err);
1046 return;
1047 };
1048 let cwd = Value::string(cwd, span);
1049
1050 let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
1051 let mut shells = if let Some(v) = shells {
1052 v.clone().into_list().unwrap_or_else(|_| vec![cwd])
1053 } else {
1054 vec![cwd]
1055 };
1056
1057 let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
1058 let current_shell = if let Some(v) = current_shell {
1059 v.as_int().unwrap_or_default() as usize
1060 } else {
1061 0
1062 };
1063
1064 let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
1065 let last_shell = if let Some(v) = last_shell {
1066 v.as_int().unwrap_or_default() as usize
1067 } else {
1068 0
1069 };
1070
1071 shells[current_shell] = Value::string(path, span);
1072
1073 stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
1074 stack.add_env_var(
1075 "NUSHELL_LAST_SHELL".into(),
1076 Value::int(last_shell as i64, span),
1077 );
1078 stack.set_last_exit_code(0, span);
1079}
1080
1081fn do_run_cmd(
1086 s: &str,
1087 stack: &mut Stack,
1088 engine_state: &mut EngineState,
1089 line_editor: Reedline,
1092 shell_integration_osc2: bool,
1093 entry_num: usize,
1094 use_color: bool,
1095) -> Reedline {
1096 trace!("eval source: {s}");
1097
1098 let had_warning_before = engine_state.exit_warning_given.load(Ordering::SeqCst);
1099
1100 if shell_integration_osc2 {
1101 run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
1102 }
1103
1104 engine_state.capture_repl_last_result = true;
1106 let eval_result = evaluate_source(
1107 engine_state,
1108 stack,
1109 s.as_bytes(),
1110 &format!("repl_entry #{entry_num}"),
1111 PipelineData::empty(),
1112 false,
1113 );
1114 engine_state.capture_repl_last_result = false;
1115
1116 match eval_result {
1117 Err(ShellError::Exit { code, .. }) => {
1118 return cleanup_exit(line_editor, engine_state, code);
1119 }
1120 Err(err) => {
1121 report_shell_error(Some(stack), engine_state, &err);
1122 stack.set_last_error(&err);
1123 }
1124 Ok(failed) => {
1125 let code: i32 = failed.into();
1126 stack.set_last_exit_code(code, Span::unknown());
1127 }
1128 }
1129
1130 if had_warning_before && engine_state.is_interactive {
1133 engine_state
1134 .exit_warning_given
1135 .store(false, Ordering::SeqCst);
1136 }
1137
1138 line_editor
1139}
1140
1141fn run_shell_integration_osc2(
1147 command_name: Option<&str>,
1148 engine_state: &EngineState,
1149 stack: &mut Stack,
1150 use_color: bool,
1151) {
1152 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1153 let start_time = Instant::now();
1154
1155 let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
1157 let home_dir_str = p.as_path().display().to_string();
1158 if path.starts_with(&home_dir_str) {
1159 path.replacen(&home_dir_str, "~", 1)
1160 } else {
1161 path
1162 }
1163 } else {
1164 path
1165 };
1166
1167 let title = match command_name {
1168 Some(binary_name) => {
1169 let split_binary_name = binary_name.split_whitespace().next();
1170 if let Some(binary_name) = split_binary_name {
1171 format!("{maybe_abbrev_path}> {binary_name}")
1172 } else {
1173 maybe_abbrev_path.to_string()
1174 }
1175 }
1176 None => maybe_abbrev_path.to_string(),
1177 };
1178
1179 run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
1185
1186 perf!("set title with command osc2", start_time, use_color);
1187 }
1188}
1189
1190fn run_shell_integration_osc7(
1191 hostname: Option<&str>,
1192 engine_state: &EngineState,
1193 stack: &mut Stack,
1194 use_color: bool,
1195) {
1196 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1197 let start_time = Instant::now();
1198
1199 let path = if cfg!(windows) {
1200 path.replace('\\', "/")
1201 } else {
1202 path
1203 };
1204
1205 run_ansi_sequence(&format!(
1207 "\x1b]7;file://{}{}{}\x1b\\",
1208 percent_encoding::utf8_percent_encode(
1209 hostname.unwrap_or("localhost"),
1210 percent_encoding::CONTROLS
1211 ),
1212 if path.starts_with('/') { "" } else { "/" },
1213 percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
1214 ));
1215
1216 perf!(
1217 "communicate path to terminal with osc7",
1218 start_time,
1219 use_color
1220 );
1221 }
1222}
1223
1224fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
1225 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1226 let start_time = Instant::now();
1227
1228 run_ansi_sequence(&format!("\x1b]9;9;{}\x1b\\", path));
1231
1232 perf!(
1233 "communicate path to terminal with osc9;9",
1234 start_time,
1235 use_color
1236 );
1237 }
1238}
1239
1240fn run_shell_integration_osc633(
1241 engine_state: &EngineState,
1242 stack: &mut Stack,
1243 use_color: bool,
1244 repl_cmd_line_text: String,
1245) {
1246 if let Ok(path) = engine_state.cwd_as_string(Some(stack)) {
1247 if stack
1250 .get_env_var(engine_state, "TERM_PROGRAM")
1251 .and_then(|v| v.as_str().ok())
1252 == Some("vscode")
1253 {
1254 let start_time = Instant::now();
1255
1256 run_ansi_sequence(&format!(
1259 "{VSCODE_CWD_PROPERTY_MARKER_PREFIX}{path}{VSCODE_CWD_PROPERTY_MARKER_SUFFIX}"
1260 ));
1261
1262 perf!(
1263 "communicate path to terminal with osc633;P",
1264 start_time,
1265 use_color
1266 );
1267
1268 let replaced_cmd_text =
1271 escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
1272
1273 run_ansi_sequence(&format!(
1275 "{VSCODE_COMMANDLINE_MARKER_PREFIX}{replaced_cmd_text}{VSCODE_COMMANDLINE_MARKER_SUFFIX}"
1276 ));
1277 }
1278 }
1279}
1280
1281fn run_shell_integration_reset_application_mode() {
1282 run_ansi_sequence(RESET_APPLICATION_MODE);
1283}
1284
1285fn flush_engine_state_repl_buffer(
1289 engine_state: &mut EngineState,
1290 mut line_editor: Reedline,
1291) -> Reedline {
1292 let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
1293 line_editor.run_edit_commands(&[
1294 EditCommand::Clear,
1295 EditCommand::InsertString(repl.buffer.to_string()),
1296 EditCommand::MoveToPosition {
1297 position: repl.cursor_pos,
1298 select: false,
1299 },
1300 ]);
1301 if repl.accept {
1302 line_editor = line_editor.with_immediately_accept(true)
1303 }
1304 repl.accept = false;
1305 repl.buffer = "".to_string();
1306 repl.cursor_pos = 0;
1307 line_editor
1308}
1309
1310fn setup_history(
1314 engine_state: &mut EngineState,
1315 line_editor: Reedline,
1316 history: HistoryConfig,
1317) -> Result<Reedline> {
1318 let history_session_id = if history.isolation {
1320 Reedline::create_history_session_id()
1321 } else {
1322 None
1323 };
1324
1325 if let Some(path) = history.file_path(&engine_state.config_dirs.config_home) {
1326 return update_line_editor_history(
1327 engine_state,
1328 path,
1329 history,
1330 line_editor,
1331 history_session_id,
1332 );
1333 };
1334 Ok(line_editor)
1335}
1336
1337fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
1341 match create_keybindings(engine_state.get_config()) {
1342 Ok(keybindings) => match keybindings {
1343 KeybindingsMode::Emacs(keybindings) => {
1344 let edit_mode = Box::new(Emacs::new(keybindings));
1345 line_editor.with_edit_mode(edit_mode)
1346 }
1347 KeybindingsMode::Vi {
1348 insert_keybindings,
1349 normal_keybindings,
1350 } => {
1351 let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
1352 line_editor.with_edit_mode(edit_mode)
1353 }
1354 #[cfg(feature = "helix")]
1355 KeybindingsMode::Helix {
1356 insert_keybindings,
1357 normal_keybindings,
1358 select_keybindings,
1359 } => {
1360 let edit_mode = Box::new(
1361 Helix::default()
1362 .with_insert_keybindings(insert_keybindings)
1363 .with_normal_keybindings(normal_keybindings)
1364 .with_select_keybindings(select_keybindings),
1365 );
1366 line_editor.with_edit_mode(edit_mode)
1367 }
1368 },
1369 Err(e) => {
1370 report_shell_error(None, engine_state, &e);
1371 line_editor
1372 }
1373 }
1374}
1375
1376fn kitty_protocol_healthcheck(engine_state: &EngineState) {
1380 if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
1381 warn!("Terminal doesn't support use_kitty_protocol config");
1382 }
1383}
1384
1385fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
1386 let session_id = line_editor
1387 .get_history_session_id()
1388 .map(i64::from)
1389 .unwrap_or(0);
1390
1391 engine_state.history_session_id = session_id;
1392}
1393
1394fn warn_history_unavailable(history_path: &std::path::Path, err: &impl std::fmt::Display) {
1395 if history_path.is_symlink() && !history_path.exists() {
1396 let target = std::fs::read_link(history_path)
1397 .map(|p| p.display().to_string())
1398 .unwrap_or_else(|_| "<unknown>".to_string());
1399 eprintln!(
1400 "Warning: history file is a broken symlink ({} -> {target}); continuing without history.",
1401 history_path.display()
1402 );
1403 } else {
1404 eprintln!(
1405 "Warning: could not open history file ({}): {err}; continuing without history.",
1406 history_path.display()
1407 );
1408 }
1409}
1410
1411fn update_line_editor_history(
1412 engine_state: &mut EngineState,
1413 history_path: PathBuf,
1414 history: HistoryConfig,
1415 line_editor: Reedline,
1416 history_session_id: Option<HistorySessionId>,
1417) -> Result<Reedline, ErrReport> {
1418 let ignore_space_prefixed = history.ignore_space_prefixed;
1419 let history_backend: Option<Box<dyn reedline::History>> = match history.file_format {
1422 HistoryFileFormat::Plaintext => {
1423 match FileBackedHistory::with_file(history.max_size as usize, history_path.clone()) {
1424 Ok(h) => Some(Box::new(h)),
1425 Err(err) => {
1426 warn_history_unavailable(&history_path, &err);
1427 None
1428 }
1429 }
1430 }
1431 #[cfg(not(feature = "sqlite"))]
1433 HistoryFileFormat::Sqlite => {
1434 return Err(miette::miette!(
1435 help = "compile Nushell with the `sqlite` feature to use this",
1436 "Unsupported history file format",
1437 ));
1438 }
1439 #[cfg(feature = "sqlite")]
1440 HistoryFileFormat::Sqlite => {
1441 match SqliteBackedHistory::with_file(
1442 history_path.clone(),
1443 history_session_id,
1444 Some(chrono::Utc::now()),
1445 ) {
1446 Ok(h) => Some(Box::new(h)),
1447 Err(err) => {
1448 warn_history_unavailable(&history_path, &err);
1449 None
1450 }
1451 }
1452 }
1453 };
1454
1455 let mut line_editor = line_editor
1456 .with_history_session_id(history_session_id)
1457 .with_history_exclusion_prefix(ignore_space_prefixed.then_some(" ".into()));
1458
1459 if let Some(history) = history_backend {
1460 line_editor = line_editor.with_history(history);
1461 }
1462
1463 store_history_id_in_engine(engine_state, &line_editor);
1464
1465 Ok(line_editor)
1466}
1467
1468fn confirm_stdin_is_terminal() -> Result<()> {
1469 if !std::io::stdin().is_terminal() {
1472 return Err(std::io::Error::new(
1473 std::io::ErrorKind::NotFound,
1474 "Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
1475 ))
1476 .into_diagnostic();
1477 }
1478 Ok(())
1479}
1480fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
1481 match shape {
1482 NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
1483 NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
1484 NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
1485 NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
1486 NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
1487 NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
1488 NuCursorShape::Inherit => None,
1489 }
1490}
1491
1492fn get_command_finished_marker(
1493 stack: &Stack,
1494 engine_state: &EngineState,
1495 shell_integration_osc633: bool,
1496 shell_integration_osc133: bool,
1497) -> String {
1498 let exit_code = stack
1499 .get_env_var(engine_state, "LAST_EXIT_CODE")
1500 .and_then(|e| e.as_int().ok());
1501
1502 if shell_integration_osc633 {
1503 if stack
1504 .get_env_var(engine_state, "TERM_PROGRAM")
1505 .and_then(|v| v.as_str().ok())
1506 == Some("vscode")
1507 {
1508 format!(
1510 "{}{}{}",
1511 VSCODE_POST_EXECUTION_MARKER_PREFIX,
1512 exit_code.unwrap_or(0),
1513 VSCODE_POST_EXECUTION_MARKER_SUFFIX
1514 )
1515 } else if shell_integration_osc133 {
1516 format!(
1518 "{}{}{}",
1519 POST_EXECUTION_MARKER_PREFIX,
1520 exit_code.unwrap_or(0),
1521 POST_EXECUTION_MARKER_SUFFIX
1522 )
1523 } else {
1524 "\x1b[0m".to_string()
1526 }
1527 } else if shell_integration_osc133 {
1528 format!(
1529 "{}{}{}",
1530 POST_EXECUTION_MARKER_PREFIX,
1531 exit_code.unwrap_or(0),
1532 POST_EXECUTION_MARKER_SUFFIX
1533 )
1534 } else {
1535 "\x1b[0m".to_string()
1536 }
1537}
1538
1539fn run_ansi_sequence(seq: &str) {
1540 if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
1541 warn!("Error writing ansi sequence {e}");
1542 } else if let Err(e) = io::stdout().flush() {
1543 warn!("Error flushing stdio {e}");
1544 }
1545}
1546
1547fn write_repl_error_details(error: &impl std::fmt::Debug) {
1548 let _ = stderr_write_all_and_flush(format!("Error: {error:?}\n"));
1549}
1550
1551fn run_finaliziation_ansi_sequence(
1552 stack: &Stack,
1553 engine_state: &EngineState,
1554 use_color: bool,
1555 shell_integration_osc633: bool,
1556 shell_integration_osc133: bool,
1557) {
1558 if shell_integration_osc633 {
1559 if stack
1561 .get_env_var(engine_state, "TERM_PROGRAM")
1562 .and_then(|v| v.as_str().ok())
1563 == Some("vscode")
1564 {
1565 let start_time = Instant::now();
1566
1567 run_ansi_sequence(&get_command_finished_marker(
1568 stack,
1569 engine_state,
1570 shell_integration_osc633,
1571 shell_integration_osc133,
1572 ));
1573
1574 perf!(
1575 "post_execute_marker (633;D) ansi escape sequences",
1576 start_time,
1577 use_color
1578 );
1579 } else if shell_integration_osc133 {
1580 let start_time = Instant::now();
1581
1582 run_ansi_sequence(&get_command_finished_marker(
1583 stack,
1584 engine_state,
1585 shell_integration_osc633,
1586 shell_integration_osc133,
1587 ));
1588
1589 perf!(
1590 "post_execute_marker (133;D) ansi escape sequences",
1591 start_time,
1592 use_color
1593 );
1594 }
1595 } else if shell_integration_osc133 {
1596 let start_time = Instant::now();
1597
1598 run_ansi_sequence(&get_command_finished_marker(
1599 stack,
1600 engine_state,
1601 shell_integration_osc633,
1602 shell_integration_osc133,
1603 ));
1604
1605 perf!(
1606 "post_execute_marker (133;D) ansi escape sequences",
1607 start_time,
1608 use_color
1609 );
1610 }
1611}
1612
1613#[cfg(windows)]
1615static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
1616 fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
1617});
1618
1619fn looks_like_path(orig: &str) -> bool {
1621 #[cfg(windows)]
1622 {
1623 if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
1624 return true;
1625 }
1626 }
1627
1628 orig.starts_with('.')
1629 || orig.starts_with('~')
1630 || orig.starts_with('/')
1631 || orig.starts_with('\\')
1632 || orig.ends_with(std::path::MAIN_SEPARATOR)
1633}
1634
1635#[cfg(test)]
1636mod semantic_marker_tests {
1637 use super::semantic_markers_from_config;
1638 use nu_protocol::Config;
1639 use reedline::PromptKind;
1640
1641 #[test]
1642 fn semantic_markers_use_osc633_in_vscode() {
1643 let mut config = Config::default();
1644 config.shell_integration.osc633 = true;
1645 config.shell_integration.osc133 = true;
1646
1647 let markers =
1648 semantic_markers_from_config(&config, true).expect("expected semantic markers");
1649
1650 assert_eq!(
1651 markers.prompt_start(PromptKind::Primary).as_ref(),
1652 "\x1b]633;A;k=i\x1b\\"
1653 );
1654 }
1655
1656 #[test]
1657 fn semantic_markers_use_osc133_click_events() {
1658 let mut config = Config::default();
1659 config.shell_integration.osc133 = true;
1660
1661 let markers =
1662 semantic_markers_from_config(&config, false).expect("expected semantic markers");
1663
1664 assert_eq!(
1665 markers.prompt_start(PromptKind::Primary).as_ref(),
1666 "\x1b]133;A;k=i;click_events=1\x1b\\"
1667 );
1668 }
1669
1670 #[test]
1671 fn semantic_markers_none_when_disabled() {
1672 let mut config = Config::default();
1673 config.shell_integration.osc133 = false;
1674 config.shell_integration.osc633 = false;
1675 assert!(semantic_markers_from_config(&config, false).is_none());
1676 }
1677}
1678
1679#[cfg(windows)]
1680#[test]
1681fn looks_like_path_windows_drive_path_works() {
1682 assert!(looks_like_path("C:"));
1683 assert!(looks_like_path("D:\\"));
1684 assert!(looks_like_path("E:/"));
1685 assert!(looks_like_path("F:\\some_dir"));
1686 assert!(looks_like_path("G:/some_dir"));
1687}
1688
1689#[cfg(windows)]
1690#[test]
1691fn trailing_slash_looks_like_path() {
1692 assert!(looks_like_path("foo\\"))
1693}
1694
1695#[cfg(not(windows))]
1696#[test]
1697fn trailing_slash_looks_like_path() {
1698 assert!(looks_like_path("foo/"))
1699}
1700
1701#[test]
1702fn are_session_ids_in_sync() {
1703 let engine_state = &mut EngineState::new();
1704 engine_state.config_dirs.config_home = std::env::temp_dir().join("nushell-repl-history-test");
1706 let history = engine_state.history_config().unwrap();
1707 let history_path = history
1708 .file_path(&engine_state.config_dirs.config_home)
1709 .unwrap();
1710 let line_editor = reedline::Reedline::create();
1711 let history_session_id = reedline::Reedline::create_history_session_id();
1712 let line_editor = update_line_editor_history(
1713 engine_state,
1714 history_path,
1715 history,
1716 line_editor,
1717 history_session_id,
1718 );
1719 assert_eq!(
1720 i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
1721 engine_state.history_session_id
1722 );
1723}
1724
1725#[cfg(unix)]
1727#[test]
1728fn dangling_history_symlink_does_not_fail_setup() {
1729 let dir = temp_dir().join(format!("nushell-dangling-history-{}", std::process::id()));
1730 let _ = std::fs::remove_dir_all(&dir);
1731 std::fs::create_dir_all(&dir).unwrap();
1732 let history_path = dir.join("history.txt");
1733 std::os::unix::fs::symlink("/nonexistent/history.txt", &history_path).unwrap();
1734
1735 let engine_state = &mut EngineState::new();
1736 engine_state.config_dirs.config_home = dir.clone();
1737 let history = engine_state.history_config().unwrap();
1738 let line_editor = reedline::Reedline::create();
1739 let history_session_id = reedline::Reedline::create_history_session_id();
1740 let result = update_line_editor_history(
1741 engine_state,
1742 history_path.clone(),
1743 history,
1744 line_editor,
1745 history_session_id,
1746 );
1747 assert!(
1748 result.is_ok(),
1749 "dangling history symlink must not fail REPL history setup"
1750 );
1751 assert!(history_path.is_symlink());
1753 let _ = std::fs::remove_dir_all(&dir);
1754}
1755
1756#[cfg(test)]
1757mod test_auto_cd {
1758 use super::{ReplOperation, do_auto_cd, escape_special_vscode_bytes, parse_operation};
1759 use nu_path::AbsolutePath;
1760 use nu_protocol::engine::{EngineState, Stack};
1761 use tempfile::tempdir;
1762
1763 #[cfg(any(unix, windows))]
1765 fn symlink(
1766 original: impl AsRef<AbsolutePath>,
1767 link: impl AsRef<AbsolutePath>,
1768 ) -> std::io::Result<()> {
1769 let original = original.as_ref();
1770 let link = link.as_ref();
1771
1772 #[cfg(unix)]
1773 {
1774 std::os::unix::fs::symlink(original, link)
1775 }
1776 #[cfg(windows)]
1777 {
1778 if original.is_dir() {
1779 std::os::windows::fs::symlink_dir(original, link)
1780 } else {
1781 std::os::windows::fs::symlink_file(original, link)
1782 }
1783 }
1784 }
1785
1786 #[track_caller]
1790 fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
1791 let mut engine_state = EngineState::new();
1793 let mut stack = Stack::new();
1794 stack.set_cwd(before.as_ref()).unwrap();
1795
1796 let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
1798 let ReplOperation::AutoCd { cwd, target, span } = op else {
1799 panic!("'{input}' was not parsed into an auto-cd operation")
1800 };
1801
1802 do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
1804 let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
1805
1806 let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
1810 let after = std::fs::canonicalize(after.as_ref()).unwrap();
1811 assert_eq!(updated_cwd, after);
1812 }
1813
1814 #[test]
1815 fn auto_cd_root() {
1816 let tempdir = tempdir().unwrap();
1817 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1818
1819 let input = if cfg!(windows) { r"C:\" } else { "/" };
1820 let root = AbsolutePath::try_new(input).unwrap();
1821 check(tempdir, input, root);
1822 }
1823
1824 #[test]
1825 fn auto_cd_tilde() {
1826 let tempdir = tempdir().unwrap();
1827 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1828
1829 let home = nu_path::home_dir().unwrap();
1830 check(tempdir, "~", home);
1831 }
1832
1833 #[test]
1834 fn auto_cd_dot() {
1835 let tempdir = tempdir().unwrap();
1836 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1837
1838 check(tempdir, ".", tempdir);
1839 }
1840
1841 #[test]
1842 fn auto_cd_double_dot() {
1843 let tempdir = tempdir().unwrap();
1844 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1845
1846 let dir = tempdir.join("foo");
1847 std::fs::create_dir_all(&dir).unwrap();
1848 check(dir, "..", tempdir);
1849 }
1850
1851 #[test]
1852 fn auto_cd_triple_dot() {
1853 let tempdir = tempdir().unwrap();
1854 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1855
1856 let dir = tempdir.join("foo").join("bar");
1857 std::fs::create_dir_all(&dir).unwrap();
1858 check(dir, "...", tempdir);
1859 }
1860
1861 #[test]
1862 fn auto_cd_relative() {
1863 let tempdir = tempdir().unwrap();
1864 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1865
1866 let foo = tempdir.join("foo");
1867 let bar = tempdir.join("bar");
1868 std::fs::create_dir_all(&foo).unwrap();
1869 std::fs::create_dir_all(&bar).unwrap();
1870 let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
1871 check(foo, input, bar);
1872 }
1873
1874 #[test]
1875 fn auto_cd_trailing_slash() {
1876 let tempdir = tempdir().unwrap();
1877 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1878
1879 let dir = tempdir.join("foo");
1880 std::fs::create_dir_all(&dir).unwrap();
1881 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1882 check(tempdir, input, dir);
1883 }
1884
1885 #[test]
1886 fn auto_cd_symlink() {
1887 let tempdir = tempdir().unwrap();
1888 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1889
1890 let dir = tempdir.join("foo");
1891 std::fs::create_dir_all(&dir).unwrap();
1892 let link = tempdir.join("link");
1893 symlink(&dir, &link).unwrap();
1894 let input = if cfg!(windows) { r".\link" } else { "./link" };
1895 check(tempdir, input, link);
1896
1897 let dir = tempdir.join("foo").join("bar");
1898 std::fs::create_dir_all(&dir).unwrap();
1899 let link = tempdir.join("link2");
1900 symlink(&dir, &link).unwrap();
1901 let input = "..";
1902 check(link, input, tempdir);
1903 }
1904
1905 #[test]
1906 #[should_panic(expected = "was not parsed into an auto-cd operation")]
1907 fn auto_cd_nonexistent_directory() {
1908 let tempdir = tempdir().unwrap();
1909 let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
1910
1911 let dir = tempdir.join("foo");
1912 let input = if cfg!(windows) { r"foo\" } else { "foo/" };
1913 check(tempdir, input, dir);
1914 }
1915
1916 #[test]
1917 fn escape_vscode_semicolon_test() {
1918 let input = "now;is";
1919 let expected = r#"now\x3Bis"#;
1920 let actual = escape_special_vscode_bytes(input).unwrap();
1921 assert_eq!(expected, actual);
1922 }
1923
1924 #[test]
1925 fn escape_vscode_backslash_test() {
1926 let input = r#"now\is"#;
1927 let expected = r#"now\\is"#;
1928 let actual = escape_special_vscode_bytes(input).unwrap();
1929 assert_eq!(expected, actual);
1930 }
1931
1932 #[test]
1933 fn escape_vscode_linefeed_test() {
1934 let input = "now\nis";
1935 let expected = r#"now\x0Ais"#;
1936 let actual = escape_special_vscode_bytes(input).unwrap();
1937 assert_eq!(expected, actual);
1938 }
1939
1940 #[test]
1941 fn escape_vscode_tab_null_cr_test() {
1942 let input = "now\t\0\ris";
1943 let expected = r#"now\x09\x00\x0Dis"#;
1944 let actual = escape_special_vscode_bytes(input).unwrap();
1945 assert_eq!(expected, actual);
1946 }
1947
1948 #[test]
1949 fn escape_vscode_multibyte_ok() {
1950 let input = "now🍪is";
1951 let actual = escape_special_vscode_bytes(input).unwrap();
1952 assert_eq!(input, actual);
1953 }
1954}