mirror of
https://github.com/nushell/nushell.git
synced 2025-01-19 04:40:41 +01:00
e87a35104a
# Description Turns out there are duplicate conversion functions: `as_i64` and `as_f64`. In most cases, these can be replaced with `as_int` and `as_float`, respectively.
1615 lines
54 KiB
Rust
1615 lines
54 KiB
Rust
use crate::prompt_update::{
|
|
POST_EXECUTION_MARKER_PREFIX, POST_EXECUTION_MARKER_SUFFIX, PRE_EXECUTION_MARKER,
|
|
RESET_APPLICATION_MODE, VSCODE_COMMANDLINE_MARKER_PREFIX, VSCODE_COMMANDLINE_MARKER_SUFFIX,
|
|
VSCODE_CWD_PROPERTY_MARKER_PREFIX, VSCODE_CWD_PROPERTY_MARKER_SUFFIX,
|
|
VSCODE_POST_EXECUTION_MARKER_PREFIX, VSCODE_POST_EXECUTION_MARKER_SUFFIX,
|
|
VSCODE_PRE_EXECUTION_MARKER,
|
|
};
|
|
use crate::{
|
|
completions::NuCompleter,
|
|
nu_highlight::NoOpHighlighter,
|
|
prompt_update,
|
|
reedline_config::{add_menus, create_keybindings, KeybindingsMode},
|
|
util::eval_source,
|
|
NuHighlighter, NuValidator, NushellPrompt,
|
|
};
|
|
use crossterm::cursor::SetCursorStyle;
|
|
use log::{error, trace, warn};
|
|
use miette::{ErrReport, IntoDiagnostic, Result};
|
|
use nu_cmd_base::{hook::eval_hook, util::get_editor};
|
|
use nu_color_config::StyleComputer;
|
|
#[allow(deprecated)]
|
|
use nu_engine::{convert_env_values, current_dir_str, env_to_strings};
|
|
use nu_parser::{lex, parse, trim_quotes_str};
|
|
use nu_protocol::{
|
|
config::NuCursorShape,
|
|
engine::{EngineState, Stack, StateWorkingSet},
|
|
report_shell_error, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned,
|
|
Value,
|
|
};
|
|
use nu_utils::{
|
|
filesystem::{have_permission, PermissionResult},
|
|
perf,
|
|
};
|
|
use reedline::{
|
|
CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
|
|
HistorySessionId, Reedline, SqliteBackedHistory, Vi,
|
|
};
|
|
use std::{
|
|
collections::HashMap,
|
|
env::temp_dir,
|
|
io::{self, IsTerminal, Write},
|
|
panic::{catch_unwind, AssertUnwindSafe},
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
time::{Duration, Instant},
|
|
};
|
|
use sysinfo::System;
|
|
|
|
/// The main REPL loop, including spinning up the prompt itself.
|
|
pub fn evaluate_repl(
|
|
engine_state: &mut EngineState,
|
|
stack: Stack,
|
|
prerun_command: Option<Spanned<String>>,
|
|
load_std_lib: Option<Spanned<String>>,
|
|
entire_start_time: Instant,
|
|
) -> Result<()> {
|
|
// throughout this code, we hold this stack uniquely.
|
|
// During the main REPL loop, we hand ownership of this value to an Arc,
|
|
// so that it may be read by various reedline plugins. During this, we
|
|
// can't modify the stack, but at the end of the loop we take back ownership
|
|
// from the Arc. This lets us avoid copying stack variables needlessly
|
|
let mut unique_stack = stack.clone();
|
|
let config = engine_state.get_config();
|
|
let use_color = config.use_ansi_coloring;
|
|
|
|
confirm_stdin_is_terminal()?;
|
|
|
|
let mut entry_num = 0;
|
|
|
|
// Let's grab the shell_integration configs
|
|
let shell_integration_osc2 = config.shell_integration.osc2;
|
|
let shell_integration_osc7 = config.shell_integration.osc7;
|
|
let shell_integration_osc9_9 = config.shell_integration.osc9_9;
|
|
let shell_integration_osc133 = config.shell_integration.osc133;
|
|
let shell_integration_osc633 = config.shell_integration.osc633;
|
|
|
|
let nu_prompt = NushellPrompt::new(
|
|
shell_integration_osc133,
|
|
shell_integration_osc633,
|
|
engine_state.clone(),
|
|
stack.clone(),
|
|
);
|
|
|
|
let start_time = std::time::Instant::now();
|
|
// Translate environment variables from Strings to Values
|
|
if let Err(e) = convert_env_values(engine_state, &unique_stack) {
|
|
report_shell_error(engine_state, &e);
|
|
}
|
|
perf!("translate env vars", start_time, use_color);
|
|
|
|
// seed env vars
|
|
unique_stack.add_env_var(
|
|
"CMD_DURATION_MS".into(),
|
|
Value::string("0823", Span::unknown()),
|
|
);
|
|
|
|
unique_stack.set_last_exit_code(0, Span::unknown());
|
|
|
|
let mut line_editor = get_line_editor(engine_state, use_color)?;
|
|
let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
|
|
|
|
if let Some(s) = prerun_command {
|
|
eval_source(
|
|
engine_state,
|
|
&mut unique_stack,
|
|
s.item.as_bytes(),
|
|
&format!("entry #{entry_num}"),
|
|
PipelineData::empty(),
|
|
false,
|
|
);
|
|
engine_state.merge_env(&mut unique_stack)?;
|
|
}
|
|
|
|
let hostname = System::host_name();
|
|
if shell_integration_osc2 {
|
|
run_shell_integration_osc2(None, engine_state, &mut unique_stack, use_color);
|
|
}
|
|
if shell_integration_osc7 {
|
|
run_shell_integration_osc7(
|
|
hostname.as_deref(),
|
|
engine_state,
|
|
&mut unique_stack,
|
|
use_color,
|
|
);
|
|
}
|
|
if shell_integration_osc9_9 {
|
|
run_shell_integration_osc9_9(engine_state, &mut unique_stack, use_color);
|
|
}
|
|
if shell_integration_osc633 {
|
|
// escape a few things because this says so
|
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
|
let cmd_text = line_editor.current_buffer_contents().to_string();
|
|
|
|
let replaced_cmd_text = escape_special_vscode_bytes(&cmd_text)?;
|
|
|
|
run_shell_integration_osc633(
|
|
engine_state,
|
|
&mut unique_stack,
|
|
use_color,
|
|
replaced_cmd_text,
|
|
);
|
|
}
|
|
|
|
engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64);
|
|
|
|
// Regenerate the $nu constant to contain the startup time and any other potential updates
|
|
engine_state.generate_nu_constant();
|
|
|
|
if load_std_lib.is_none() && engine_state.get_config().show_banner {
|
|
eval_source(
|
|
engine_state,
|
|
&mut unique_stack,
|
|
r#"banner"#.as_bytes(),
|
|
"show_banner",
|
|
PipelineData::empty(),
|
|
false,
|
|
);
|
|
}
|
|
|
|
kitty_protocol_healthcheck(engine_state);
|
|
|
|
// Setup initial engine_state and stack state
|
|
let mut previous_engine_state = engine_state.clone();
|
|
let mut previous_stack_arc = Arc::new(unique_stack);
|
|
loop {
|
|
// clone these values so that they can be moved by AssertUnwindSafe
|
|
// If there is a panic within this iteration the last engine_state and stack
|
|
// will be used
|
|
let mut current_engine_state = previous_engine_state.clone();
|
|
// for the stack, we are going to hold to create a child stack instead,
|
|
// avoiding an expensive copy
|
|
let current_stack = Stack::with_parent(previous_stack_arc.clone());
|
|
let temp_file_cloned = temp_file.clone();
|
|
let mut nu_prompt_cloned = nu_prompt.clone();
|
|
|
|
let iteration_panic_state = catch_unwind(AssertUnwindSafe(|| {
|
|
let (continue_loop, current_stack, line_editor) = loop_iteration(LoopContext {
|
|
engine_state: &mut current_engine_state,
|
|
stack: current_stack,
|
|
line_editor,
|
|
nu_prompt: &mut nu_prompt_cloned,
|
|
temp_file: &temp_file_cloned,
|
|
use_color,
|
|
entry_num: &mut entry_num,
|
|
hostname: hostname.as_deref(),
|
|
});
|
|
|
|
// pass the most recent version of the line_editor back
|
|
(
|
|
continue_loop,
|
|
current_engine_state,
|
|
current_stack,
|
|
line_editor,
|
|
)
|
|
}));
|
|
match iteration_panic_state {
|
|
Ok((continue_loop, es, s, le)) => {
|
|
// setup state for the next iteration of the repl loop
|
|
previous_engine_state = es;
|
|
// we apply the changes from the updated stack back onto our previous stack
|
|
previous_stack_arc =
|
|
Arc::new(Stack::with_changes_from_child(previous_stack_arc, s));
|
|
line_editor = le;
|
|
if !continue_loop {
|
|
break;
|
|
}
|
|
}
|
|
Err(_) => {
|
|
// line_editor is lost in the error case so reconstruct a new one
|
|
line_editor = get_line_editor(engine_state, use_color)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn escape_special_vscode_bytes(input: &str) -> Result<String, ShellError> {
|
|
let bytes = input
|
|
.chars()
|
|
.flat_map(|c| {
|
|
let mut buf = [0; 4]; // Buffer to hold UTF-8 bytes of the character
|
|
let c_bytes = c.encode_utf8(&mut buf); // Get UTF-8 bytes for the character
|
|
|
|
if c_bytes.len() == 1 {
|
|
let byte = c_bytes.as_bytes()[0];
|
|
|
|
match byte {
|
|
// Escape bytes below 0x20
|
|
b if b < 0x20 => format!("\\x{:02X}", byte).into_bytes(),
|
|
// Escape semicolon as \x3B
|
|
b';' => "\\x3B".to_string().into_bytes(),
|
|
// Escape backslash as \\
|
|
b'\\' => "\\\\".to_string().into_bytes(),
|
|
// Otherwise, return the character unchanged
|
|
_ => vec![byte],
|
|
}
|
|
} else {
|
|
// pass through multi-byte characters unchanged
|
|
c_bytes.bytes().collect()
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
String::from_utf8(bytes).map_err(|err| ShellError::CantConvert {
|
|
to_type: "string".to_string(),
|
|
from_type: "bytes".to_string(),
|
|
span: Span::unknown(),
|
|
help: Some(format!(
|
|
"Error {err}, Unable to convert {input} to escaped bytes"
|
|
)),
|
|
})
|
|
}
|
|
|
|
fn get_line_editor(engine_state: &mut EngineState, use_color: bool) -> Result<Reedline> {
|
|
let mut start_time = std::time::Instant::now();
|
|
let mut line_editor = Reedline::create();
|
|
|
|
// Now that reedline is created, get the history session id and store it in engine_state
|
|
store_history_id_in_engine(engine_state, &line_editor);
|
|
perf!("setup reedline", start_time, use_color);
|
|
|
|
if let Some(history) = engine_state.history_config() {
|
|
start_time = std::time::Instant::now();
|
|
|
|
line_editor = setup_history(engine_state, line_editor, history)?;
|
|
|
|
perf!("setup history", start_time, use_color);
|
|
}
|
|
Ok(line_editor)
|
|
}
|
|
|
|
struct LoopContext<'a> {
|
|
engine_state: &'a mut EngineState,
|
|
stack: Stack,
|
|
line_editor: Reedline,
|
|
nu_prompt: &'a mut NushellPrompt,
|
|
temp_file: &'a Path,
|
|
use_color: bool,
|
|
entry_num: &'a mut usize,
|
|
hostname: Option<&'a str>,
|
|
}
|
|
|
|
/// Perform one iteration of the REPL loop
|
|
/// Result is bool: continue loop, current reedline
|
|
#[inline]
|
|
fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
|
use nu_cmd_base::hook;
|
|
use reedline::Signal;
|
|
let loop_start_time = std::time::Instant::now();
|
|
|
|
let LoopContext {
|
|
engine_state,
|
|
mut stack,
|
|
line_editor,
|
|
nu_prompt,
|
|
temp_file,
|
|
use_color,
|
|
entry_num,
|
|
hostname,
|
|
} = ctx;
|
|
|
|
let mut start_time = std::time::Instant::now();
|
|
// Before doing anything, merge the environment from the previous REPL iteration into the
|
|
// permanent state.
|
|
if let Err(err) = engine_state.merge_env(&mut stack) {
|
|
report_shell_error(engine_state, &err);
|
|
}
|
|
// Check whether $env.NU_DISABLE_IR is set, so that the user can change it in the REPL
|
|
// Temporary while IR eval is optional
|
|
stack.use_ir = !stack.has_env_var(engine_state, "NU_DISABLE_IR");
|
|
perf!("merge env", start_time, use_color);
|
|
|
|
start_time = std::time::Instant::now();
|
|
engine_state.reset_signals();
|
|
perf!("reset signals", start_time, use_color);
|
|
|
|
start_time = std::time::Instant::now();
|
|
// Right before we start our prompt and take input from the user,
|
|
// fire the "pre_prompt" hook
|
|
if let Some(hook) = engine_state.get_config().hooks.pre_prompt.clone() {
|
|
if let Err(err) = eval_hook(engine_state, &mut stack, None, vec![], &hook, "pre_prompt") {
|
|
report_shell_error(engine_state, &err);
|
|
}
|
|
}
|
|
perf!("pre-prompt hook", start_time, use_color);
|
|
|
|
start_time = std::time::Instant::now();
|
|
// Next, check all the environment variables they ask for
|
|
// fire the "env_change" hook
|
|
let env_change = engine_state.get_config().hooks.env_change.clone();
|
|
if let Err(error) = hook::eval_env_change_hook(env_change, engine_state, &mut stack) {
|
|
report_shell_error(engine_state, &error)
|
|
}
|
|
perf!("env-change hook", start_time, use_color);
|
|
|
|
let engine_reference = Arc::new(engine_state.clone());
|
|
let config = stack.get_config(engine_state);
|
|
|
|
start_time = std::time::Instant::now();
|
|
// Find the configured cursor shapes for each mode
|
|
let cursor_config = CursorConfig {
|
|
vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_insert),
|
|
vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape.vi_normal),
|
|
emacs: map_nucursorshape_to_cursorshape(config.cursor_shape.emacs),
|
|
};
|
|
perf!("get config/cursor config", start_time, use_color);
|
|
|
|
start_time = std::time::Instant::now();
|
|
// at this line we have cloned the state for the completer and the transient prompt
|
|
// until we drop those, we cannot use the stack in the REPL loop itself
|
|
// See STACK-REFERENCE to see where we have taken a reference
|
|
let stack_arc = Arc::new(stack);
|
|
|
|
let mut line_editor = line_editor
|
|
.use_kitty_keyboard_enhancement(config.use_kitty_protocol)
|
|
// try to enable bracketed paste
|
|
// It doesn't work on windows system: https://github.com/crossterm-rs/crossterm/issues/737
|
|
.use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
|
|
.with_highlighter(Box::new(NuHighlighter {
|
|
engine_state: engine_reference.clone(),
|
|
// STACK-REFERENCE 1
|
|
stack: stack_arc.clone(),
|
|
}))
|
|
.with_validator(Box::new(NuValidator {
|
|
engine_state: engine_reference.clone(),
|
|
}))
|
|
.with_completer(Box::new(NuCompleter::new(
|
|
engine_reference.clone(),
|
|
// STACK-REFERENCE 2
|
|
stack_arc.clone(),
|
|
)))
|
|
.with_quick_completions(config.completions.quick)
|
|
.with_partial_completions(config.completions.partial)
|
|
.with_ansi_colors(config.use_ansi_coloring)
|
|
.with_cwd(Some(
|
|
engine_state
|
|
.cwd(None)
|
|
.map(|cwd| cwd.into_std_path_buf())
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_string(),
|
|
))
|
|
.with_cursor_config(cursor_config)
|
|
.with_visual_selection_style(nu_ansi_term::Style {
|
|
is_reverse: true,
|
|
..Default::default()
|
|
});
|
|
|
|
perf!("reedline builder", start_time, use_color);
|
|
|
|
let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
|
|
|
|
start_time = std::time::Instant::now();
|
|
line_editor = if config.use_ansi_coloring {
|
|
line_editor.with_hinter(Box::new({
|
|
// As of Nov 2022, "hints" color_config closures only get `null` passed in.
|
|
let style = style_computer.compute("hints", &Value::nothing(Span::unknown()));
|
|
CwdAwareHinter::default().with_style(style)
|
|
}))
|
|
} else {
|
|
line_editor.disable_hints()
|
|
};
|
|
|
|
perf!("reedline coloring/style_computer", start_time, use_color);
|
|
|
|
start_time = std::time::Instant::now();
|
|
trace!("adding menus");
|
|
line_editor =
|
|
add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
|
|
report_shell_error(engine_state, &e);
|
|
Reedline::create()
|
|
});
|
|
|
|
perf!("reedline adding menus", start_time, use_color);
|
|
|
|
start_time = std::time::Instant::now();
|
|
let buffer_editor = get_editor(engine_state, &stack_arc, Span::unknown());
|
|
|
|
line_editor = if let Ok((cmd, args)) = buffer_editor {
|
|
let mut command = std::process::Command::new(cmd);
|
|
let envs = env_to_strings(engine_state, &stack_arc).unwrap_or_else(|e| {
|
|
warn!("Couldn't convert environment variable values to strings: {e}");
|
|
HashMap::default()
|
|
});
|
|
command.args(args).envs(envs);
|
|
line_editor.with_buffer_editor(command, temp_file.to_path_buf())
|
|
} else {
|
|
line_editor
|
|
};
|
|
|
|
perf!("reedline buffer_editor", start_time, use_color);
|
|
|
|
if let Some(history) = engine_state.history_config() {
|
|
start_time = std::time::Instant::now();
|
|
if history.sync_on_enter {
|
|
if let Err(e) = line_editor.sync_history() {
|
|
warn!("Failed to sync history: {}", e);
|
|
}
|
|
}
|
|
|
|
perf!("sync_history", start_time, use_color);
|
|
}
|
|
|
|
start_time = std::time::Instant::now();
|
|
// Changing the line editor based on the found keybindings
|
|
line_editor = setup_keybindings(engine_state, line_editor);
|
|
|
|
perf!("keybindings", start_time, use_color);
|
|
|
|
start_time = std::time::Instant::now();
|
|
let config = &engine_state.get_config().clone();
|
|
prompt_update::update_prompt(
|
|
config,
|
|
engine_state,
|
|
&mut Stack::with_parent(stack_arc.clone()),
|
|
nu_prompt,
|
|
);
|
|
let transient_prompt = prompt_update::make_transient_prompt(
|
|
config,
|
|
engine_state,
|
|
&mut Stack::with_parent(stack_arc.clone()),
|
|
nu_prompt,
|
|
);
|
|
|
|
perf!("update_prompt", start_time, use_color);
|
|
|
|
*entry_num += 1;
|
|
|
|
start_time = std::time::Instant::now();
|
|
line_editor = line_editor.with_transient_prompt(transient_prompt);
|
|
let input = line_editor.read_line(nu_prompt);
|
|
// we got our inputs, we can now drop our stack references
|
|
// This lists all of the stack references that we have cleaned up
|
|
line_editor = line_editor
|
|
// CLEAR STACK-REFERENCE 1
|
|
.with_highlighter(Box::<NoOpHighlighter>::default())
|
|
// CLEAR STACK-REFERENCE 2
|
|
.with_completer(Box::<DefaultCompleter>::default());
|
|
|
|
// Let's grab the shell_integration configs
|
|
let shell_integration_osc2 = config.shell_integration.osc2;
|
|
let shell_integration_osc7 = config.shell_integration.osc7;
|
|
let shell_integration_osc9_9 = config.shell_integration.osc9_9;
|
|
let shell_integration_osc133 = config.shell_integration.osc133;
|
|
let shell_integration_osc633 = config.shell_integration.osc633;
|
|
let shell_integration_reset_application_mode = config.shell_integration.reset_application_mode;
|
|
|
|
// TODO: we may clone the stack, this can lead to major performance issues
|
|
// so we should avoid it or making stack cheaper to clone.
|
|
let mut stack = Arc::unwrap_or_clone(stack_arc);
|
|
|
|
perf!("line_editor setup", start_time, use_color);
|
|
|
|
let line_editor_input_time = std::time::Instant::now();
|
|
match input {
|
|
Ok(Signal::Success(repl_cmd_line_text)) => {
|
|
let history_supports_meta = matches!(
|
|
engine_state.history_config().map(|h| h.file_format),
|
|
Some(HistoryFileFormat::Sqlite)
|
|
);
|
|
|
|
if history_supports_meta {
|
|
prepare_history_metadata(
|
|
&repl_cmd_line_text,
|
|
hostname,
|
|
engine_state,
|
|
&mut line_editor,
|
|
);
|
|
}
|
|
|
|
// For pre_exec_hook
|
|
start_time = Instant::now();
|
|
|
|
// Right before we start running the code the user gave us, fire the `pre_execution`
|
|
// hook
|
|
if let Some(hook) = config.hooks.pre_execution.clone() {
|
|
// Set the REPL buffer to the current command for the "pre_execution" hook
|
|
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
|
|
repl.buffer = repl_cmd_line_text.to_string();
|
|
drop(repl);
|
|
|
|
if let Err(err) = eval_hook(
|
|
engine_state,
|
|
&mut stack,
|
|
None,
|
|
vec![],
|
|
&hook,
|
|
"pre_execution",
|
|
) {
|
|
report_shell_error(engine_state, &err);
|
|
}
|
|
}
|
|
|
|
perf!("pre_execution_hook", start_time, use_color);
|
|
|
|
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
|
|
repl.cursor_pos = line_editor.current_insertion_point();
|
|
repl.buffer = line_editor.current_buffer_contents().to_string();
|
|
drop(repl);
|
|
|
|
if shell_integration_osc633 {
|
|
if stack
|
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
|
.and_then(|v| v.as_str().ok())
|
|
== Some("vscode")
|
|
{
|
|
start_time = Instant::now();
|
|
|
|
run_ansi_sequence(VSCODE_PRE_EXECUTION_MARKER);
|
|
|
|
perf!(
|
|
"pre_execute_marker (633;C) ansi escape sequence",
|
|
start_time,
|
|
use_color
|
|
);
|
|
} else if shell_integration_osc133 {
|
|
start_time = Instant::now();
|
|
|
|
run_ansi_sequence(PRE_EXECUTION_MARKER);
|
|
|
|
perf!(
|
|
"pre_execute_marker (133;C) ansi escape sequence",
|
|
start_time,
|
|
use_color
|
|
);
|
|
}
|
|
} else if shell_integration_osc133 {
|
|
start_time = Instant::now();
|
|
|
|
run_ansi_sequence(PRE_EXECUTION_MARKER);
|
|
|
|
perf!(
|
|
"pre_execute_marker (133;C) ansi escape sequence",
|
|
start_time,
|
|
use_color
|
|
);
|
|
}
|
|
|
|
// Actual command execution logic starts from here
|
|
let cmd_execution_start_time = Instant::now();
|
|
|
|
match parse_operation(repl_cmd_line_text.clone(), engine_state, &stack) {
|
|
Ok(operation) => match operation {
|
|
ReplOperation::AutoCd { cwd, target, span } => {
|
|
do_auto_cd(target, cwd, &mut stack, engine_state, span);
|
|
|
|
run_finaliziation_ansi_sequence(
|
|
&stack,
|
|
engine_state,
|
|
use_color,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
);
|
|
}
|
|
ReplOperation::RunCommand(cmd) => {
|
|
line_editor = do_run_cmd(
|
|
&cmd,
|
|
&mut stack,
|
|
engine_state,
|
|
line_editor,
|
|
shell_integration_osc2,
|
|
*entry_num,
|
|
use_color,
|
|
);
|
|
|
|
run_finaliziation_ansi_sequence(
|
|
&stack,
|
|
engine_state,
|
|
use_color,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
);
|
|
}
|
|
// as the name implies, we do nothing in this case
|
|
ReplOperation::DoNothing => {}
|
|
},
|
|
Err(ref e) => error!("Error parsing operation: {e}"),
|
|
}
|
|
let cmd_duration = cmd_execution_start_time.elapsed();
|
|
|
|
stack.add_env_var(
|
|
"CMD_DURATION_MS".into(),
|
|
Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
|
|
);
|
|
|
|
if history_supports_meta {
|
|
if let Err(e) = fill_in_result_related_history_metadata(
|
|
&repl_cmd_line_text,
|
|
engine_state,
|
|
cmd_duration,
|
|
&mut stack,
|
|
&mut line_editor,
|
|
) {
|
|
warn!("Could not fill in result related history metadata: {e}");
|
|
}
|
|
}
|
|
|
|
if shell_integration_osc2 {
|
|
run_shell_integration_osc2(None, engine_state, &mut stack, use_color);
|
|
}
|
|
if shell_integration_osc7 {
|
|
run_shell_integration_osc7(hostname, engine_state, &mut stack, use_color);
|
|
}
|
|
if shell_integration_osc9_9 {
|
|
run_shell_integration_osc9_9(engine_state, &mut stack, use_color);
|
|
}
|
|
if shell_integration_osc633 {
|
|
run_shell_integration_osc633(
|
|
engine_state,
|
|
&mut stack,
|
|
use_color,
|
|
repl_cmd_line_text,
|
|
);
|
|
}
|
|
if shell_integration_reset_application_mode {
|
|
run_shell_integration_reset_application_mode();
|
|
}
|
|
|
|
flush_engine_state_repl_buffer(engine_state, &mut line_editor);
|
|
}
|
|
Ok(Signal::CtrlC) => {
|
|
// `Reedline` clears the line content. New prompt is shown
|
|
run_finaliziation_ansi_sequence(
|
|
&stack,
|
|
engine_state,
|
|
use_color,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
);
|
|
}
|
|
Ok(Signal::CtrlD) => {
|
|
// When exiting clear to a new line
|
|
|
|
run_finaliziation_ansi_sequence(
|
|
&stack,
|
|
engine_state,
|
|
use_color,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
);
|
|
|
|
println!();
|
|
return (false, stack, line_editor);
|
|
}
|
|
Err(err) => {
|
|
let message = err.to_string();
|
|
if !message.contains("duration") {
|
|
eprintln!("Error: {err:?}");
|
|
// TODO: Identify possible error cases where a hard failure is preferable
|
|
// Ignoring and reporting could hide bigger problems
|
|
// e.g. https://github.com/nushell/nushell/issues/6452
|
|
// Alternatively only allow that expected failures let the REPL loop
|
|
}
|
|
|
|
run_finaliziation_ansi_sequence(
|
|
&stack,
|
|
engine_state,
|
|
use_color,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
);
|
|
}
|
|
}
|
|
perf!(
|
|
"processing line editor input",
|
|
line_editor_input_time,
|
|
use_color
|
|
);
|
|
|
|
perf!(
|
|
"time between prompts in line editor loop",
|
|
loop_start_time,
|
|
use_color
|
|
);
|
|
|
|
(true, stack, line_editor)
|
|
}
|
|
|
|
///
|
|
/// Put in history metadata not related to the result of running the command
|
|
///
|
|
fn prepare_history_metadata(
|
|
s: &str,
|
|
hostname: Option<&str>,
|
|
engine_state: &EngineState,
|
|
line_editor: &mut Reedline,
|
|
) {
|
|
if !s.is_empty() && line_editor.has_last_command_context() {
|
|
let result = line_editor
|
|
.update_last_command_context(&|mut c| {
|
|
c.start_timestamp = Some(chrono::Utc::now());
|
|
c.hostname = hostname.map(str::to_string);
|
|
c.cwd = engine_state
|
|
.cwd(None)
|
|
.ok()
|
|
.map(|path| path.to_string_lossy().to_string());
|
|
c
|
|
})
|
|
.into_diagnostic();
|
|
if let Err(e) = result {
|
|
warn!("Could not prepare history metadata: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Fills in history item metadata based on the execution result (notably duration and exit code)
|
|
///
|
|
fn fill_in_result_related_history_metadata(
|
|
s: &str,
|
|
engine_state: &EngineState,
|
|
cmd_duration: Duration,
|
|
stack: &mut Stack,
|
|
line_editor: &mut Reedline,
|
|
) -> Result<()> {
|
|
if !s.is_empty() && line_editor.has_last_command_context() {
|
|
line_editor
|
|
.update_last_command_context(&|mut c| {
|
|
c.duration = Some(cmd_duration);
|
|
c.exit_status = stack
|
|
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
|
.and_then(|e| e.as_int().ok());
|
|
c
|
|
})
|
|
.into_diagnostic()?; // todo: don't stop repl if error here?
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The kinds of operations you can do in a single loop iteration of the REPL
|
|
enum ReplOperation {
|
|
/// "auto-cd": change directory by typing it in directly
|
|
AutoCd {
|
|
/// the current working directory
|
|
cwd: String,
|
|
/// the target
|
|
target: PathBuf,
|
|
/// span information for debugging
|
|
span: Span,
|
|
},
|
|
/// run a command
|
|
RunCommand(String),
|
|
/// do nothing (usually through an empty string)
|
|
DoNothing,
|
|
}
|
|
|
|
///
|
|
/// Parses one "REPL line" of input, to try and derive intent.
|
|
/// Notably, this is where we detect whether the user is attempting an
|
|
/// "auto-cd" (writing a relative path directly instead of `cd path`)
|
|
///
|
|
/// Returns the ReplOperation we believe the user wants to do
|
|
///
|
|
fn parse_operation(
|
|
s: String,
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
) -> Result<ReplOperation, ErrReport> {
|
|
let tokens = lex(s.as_bytes(), 0, &[], &[], false);
|
|
// Check if this is a single call to a directory, if so auto-cd
|
|
#[allow(deprecated)]
|
|
let cwd = nu_engine::env::current_dir_str(engine_state, stack).unwrap_or_default();
|
|
let mut orig = s.clone();
|
|
if orig.starts_with('`') {
|
|
orig = trim_quotes_str(&orig).to_string()
|
|
}
|
|
|
|
let path = nu_path::expand_path_with(&orig, &cwd, true);
|
|
if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 {
|
|
Ok(ReplOperation::AutoCd {
|
|
cwd,
|
|
target: path,
|
|
span: tokens.0[0].span,
|
|
})
|
|
} else if !s.trim().is_empty() {
|
|
Ok(ReplOperation::RunCommand(s))
|
|
} else {
|
|
Ok(ReplOperation::DoNothing)
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Execute an "auto-cd" operation, changing the current working directory.
|
|
///
|
|
fn do_auto_cd(
|
|
path: PathBuf,
|
|
cwd: String,
|
|
stack: &mut Stack,
|
|
engine_state: &mut EngineState,
|
|
span: Span,
|
|
) {
|
|
let path = {
|
|
if !path.exists() {
|
|
report_shell_error(
|
|
engine_state,
|
|
&ShellError::DirectoryNotFound {
|
|
dir: path.to_string_lossy().to_string(),
|
|
span,
|
|
},
|
|
);
|
|
}
|
|
path.to_string_lossy().to_string()
|
|
};
|
|
|
|
if let PermissionResult::PermissionDenied(reason) = have_permission(path.clone()) {
|
|
report_shell_error(
|
|
engine_state,
|
|
&ShellError::IOError {
|
|
msg: format!("Cannot change directory to {path}: {reason}"),
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown()));
|
|
|
|
//FIXME: this only changes the current scope, but instead this environment variable
|
|
//should probably be a block that loads the information from the state in the overlay
|
|
if let Err(err) = stack.set_cwd(&path) {
|
|
report_shell_error(engine_state, &err);
|
|
return;
|
|
};
|
|
let cwd = Value::string(cwd, span);
|
|
|
|
let shells = stack.get_env_var(engine_state, "NUSHELL_SHELLS");
|
|
let mut shells = if let Some(v) = shells {
|
|
v.clone().into_list().unwrap_or_else(|_| vec![cwd])
|
|
} else {
|
|
vec![cwd]
|
|
};
|
|
|
|
let current_shell = stack.get_env_var(engine_state, "NUSHELL_CURRENT_SHELL");
|
|
let current_shell = if let Some(v) = current_shell {
|
|
v.as_int().unwrap_or_default() as usize
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let last_shell = stack.get_env_var(engine_state, "NUSHELL_LAST_SHELL");
|
|
let last_shell = if let Some(v) = last_shell {
|
|
v.as_int().unwrap_or_default() as usize
|
|
} else {
|
|
0
|
|
};
|
|
|
|
shells[current_shell] = Value::string(path, span);
|
|
|
|
stack.add_env_var("NUSHELL_SHELLS".into(), Value::list(shells, span));
|
|
stack.add_env_var(
|
|
"NUSHELL_LAST_SHELL".into(),
|
|
Value::int(last_shell as i64, span),
|
|
);
|
|
stack.set_last_exit_code(0, Span::unknown());
|
|
}
|
|
|
|
///
|
|
/// Run a command as received from reedline. This is where we are actually
|
|
/// running a thing!
|
|
///
|
|
fn do_run_cmd(
|
|
s: &str,
|
|
stack: &mut Stack,
|
|
engine_state: &mut EngineState,
|
|
// we pass in the line editor so it can be dropped in the case of a process exit
|
|
// (in the normal case we don't want to drop it so return it as-is otherwise)
|
|
line_editor: Reedline,
|
|
shell_integration_osc2: bool,
|
|
entry_num: usize,
|
|
use_color: bool,
|
|
) -> Reedline {
|
|
trace!("eval source: {}", s);
|
|
|
|
let mut cmds = s.split_whitespace();
|
|
if let Some("exit") = cmds.next() {
|
|
let mut working_set = StateWorkingSet::new(engine_state);
|
|
let _ = parse(&mut working_set, None, s.as_bytes(), false);
|
|
|
|
if working_set.parse_errors.is_empty() {
|
|
match cmds.next() {
|
|
Some(s) => {
|
|
if let Ok(n) = s.parse::<i32>() {
|
|
drop(line_editor);
|
|
std::process::exit(n);
|
|
}
|
|
}
|
|
None => {
|
|
drop(line_editor);
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if shell_integration_osc2 {
|
|
run_shell_integration_osc2(Some(s), engine_state, stack, use_color);
|
|
}
|
|
|
|
eval_source(
|
|
engine_state,
|
|
stack,
|
|
s.as_bytes(),
|
|
&format!("entry #{entry_num}"),
|
|
PipelineData::empty(),
|
|
false,
|
|
);
|
|
|
|
line_editor
|
|
}
|
|
|
|
///
|
|
/// Output some things and set environment variables so shells with the right integration
|
|
/// can have more information about what is going on (both on startup and after we have
|
|
/// run a command)
|
|
///
|
|
fn run_shell_integration_osc2(
|
|
command_name: Option<&str>,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
use_color: bool,
|
|
) {
|
|
#[allow(deprecated)]
|
|
if let Ok(path) = current_dir_str(engine_state, stack) {
|
|
let start_time = Instant::now();
|
|
|
|
// Try to abbreviate string for windows title
|
|
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
|
|
let home_dir_str = p.as_path().display().to_string();
|
|
if path.starts_with(&home_dir_str) {
|
|
path.replacen(&home_dir_str, "~", 1)
|
|
} else {
|
|
path
|
|
}
|
|
} else {
|
|
path
|
|
};
|
|
|
|
let title = match command_name {
|
|
Some(binary_name) => {
|
|
let split_binary_name = binary_name.split_whitespace().next();
|
|
if let Some(binary_name) = split_binary_name {
|
|
format!("{maybe_abbrev_path}> {binary_name}")
|
|
} else {
|
|
maybe_abbrev_path.to_string()
|
|
}
|
|
}
|
|
None => maybe_abbrev_path.to_string(),
|
|
};
|
|
|
|
// Set window title too
|
|
// https://tldp.org/HOWTO/Xterm-Title-3.html
|
|
// ESC]0;stringBEL -- Set icon name and window title to string
|
|
// ESC]1;stringBEL -- Set icon name to string
|
|
// ESC]2;stringBEL -- Set window title to string
|
|
run_ansi_sequence(&format!("\x1b]2;{title}\x07"));
|
|
|
|
perf!("set title with command osc2", start_time, use_color);
|
|
}
|
|
}
|
|
|
|
fn run_shell_integration_osc7(
|
|
hostname: Option<&str>,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
use_color: bool,
|
|
) {
|
|
#[allow(deprecated)]
|
|
if let Ok(path) = current_dir_str(engine_state, stack) {
|
|
let start_time = Instant::now();
|
|
|
|
// Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
|
|
run_ansi_sequence(&format!(
|
|
"\x1b]7;file://{}{}{}\x1b\\",
|
|
percent_encoding::utf8_percent_encode(
|
|
hostname.unwrap_or("localhost"),
|
|
percent_encoding::CONTROLS
|
|
),
|
|
if path.starts_with('/') { "" } else { "/" },
|
|
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
|
|
));
|
|
|
|
perf!(
|
|
"communicate path to terminal with osc7",
|
|
start_time,
|
|
use_color
|
|
);
|
|
}
|
|
}
|
|
|
|
fn run_shell_integration_osc9_9(engine_state: &EngineState, stack: &mut Stack, use_color: bool) {
|
|
#[allow(deprecated)]
|
|
if let Ok(path) = current_dir_str(engine_state, stack) {
|
|
let start_time = Instant::now();
|
|
|
|
// Otherwise, communicate the path as OSC 9;9 from ConEmu (often used for spawning new tabs in the same dir)
|
|
// This is helpful in Windows Terminal with Duplicate Tab
|
|
run_ansi_sequence(&format!(
|
|
"\x1b]9;9;{}\x1b\\",
|
|
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
|
|
));
|
|
|
|
perf!(
|
|
"communicate path to terminal with osc9;9",
|
|
start_time,
|
|
use_color
|
|
);
|
|
}
|
|
}
|
|
|
|
fn run_shell_integration_osc633(
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
use_color: bool,
|
|
repl_cmd_line_text: String,
|
|
) {
|
|
#[allow(deprecated)]
|
|
if let Ok(path) = current_dir_str(engine_state, stack) {
|
|
// Supported escape sequences of Microsoft's Visual Studio Code (vscode)
|
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
|
|
if stack
|
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
|
.and_then(|v| v.as_str().ok())
|
|
== Some("vscode")
|
|
{
|
|
let start_time = Instant::now();
|
|
|
|
// If we're in vscode, run their specific ansi escape sequence.
|
|
// This is helpful for ctrl+g to change directories in the terminal.
|
|
run_ansi_sequence(&format!(
|
|
"{}{}{}",
|
|
VSCODE_CWD_PROPERTY_MARKER_PREFIX, path, VSCODE_CWD_PROPERTY_MARKER_SUFFIX
|
|
));
|
|
|
|
perf!(
|
|
"communicate path to terminal with osc633;P",
|
|
start_time,
|
|
use_color
|
|
);
|
|
|
|
// escape a few things because this says so
|
|
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
|
|
let replaced_cmd_text =
|
|
escape_special_vscode_bytes(&repl_cmd_line_text).unwrap_or(repl_cmd_line_text);
|
|
|
|
//OSC 633 ; E ; <commandline> [; <nonce] ST - Explicitly set the command line with an optional nonce.
|
|
run_ansi_sequence(&format!(
|
|
"{}{}{}",
|
|
VSCODE_COMMANDLINE_MARKER_PREFIX,
|
|
replaced_cmd_text,
|
|
VSCODE_COMMANDLINE_MARKER_SUFFIX
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_shell_integration_reset_application_mode() {
|
|
run_ansi_sequence(RESET_APPLICATION_MODE);
|
|
}
|
|
|
|
///
|
|
/// Clear the screen and output anything remaining in the EngineState buffer.
|
|
///
|
|
fn flush_engine_state_repl_buffer(engine_state: &mut EngineState, line_editor: &mut Reedline) {
|
|
let mut repl = engine_state.repl_state.lock().expect("repl state mutex");
|
|
line_editor.run_edit_commands(&[
|
|
EditCommand::Clear,
|
|
EditCommand::InsertString(repl.buffer.to_string()),
|
|
EditCommand::MoveToPosition {
|
|
position: repl.cursor_pos,
|
|
select: false,
|
|
},
|
|
]);
|
|
repl.buffer = "".to_string();
|
|
repl.cursor_pos = 0;
|
|
}
|
|
|
|
///
|
|
/// Setup history management for Reedline
|
|
///
|
|
fn setup_history(
|
|
engine_state: &mut EngineState,
|
|
line_editor: Reedline,
|
|
history: HistoryConfig,
|
|
) -> Result<Reedline> {
|
|
// Setup history_isolation aka "history per session"
|
|
let history_session_id = if history.isolation {
|
|
Reedline::create_history_session_id()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(path) = history.file_path() {
|
|
return update_line_editor_history(
|
|
engine_state,
|
|
path,
|
|
history,
|
|
line_editor,
|
|
history_session_id,
|
|
);
|
|
};
|
|
Ok(line_editor)
|
|
}
|
|
|
|
///
|
|
/// Setup Reedline keybindingds based on the provided config
|
|
///
|
|
fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedline {
|
|
return match create_keybindings(engine_state.get_config()) {
|
|
Ok(keybindings) => match keybindings {
|
|
KeybindingsMode::Emacs(keybindings) => {
|
|
let edit_mode = Box::new(Emacs::new(keybindings));
|
|
line_editor.with_edit_mode(edit_mode)
|
|
}
|
|
KeybindingsMode::Vi {
|
|
insert_keybindings,
|
|
normal_keybindings,
|
|
} => {
|
|
let edit_mode = Box::new(Vi::new(insert_keybindings, normal_keybindings));
|
|
line_editor.with_edit_mode(edit_mode)
|
|
}
|
|
},
|
|
Err(e) => {
|
|
report_shell_error(engine_state, &e);
|
|
line_editor
|
|
}
|
|
};
|
|
}
|
|
|
|
///
|
|
/// Make sure that the terminal supports the kitty protocol if the config is asking for it
|
|
///
|
|
fn kitty_protocol_healthcheck(engine_state: &EngineState) {
|
|
if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
|
|
warn!("Terminal doesn't support use_kitty_protocol config");
|
|
}
|
|
}
|
|
|
|
fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
|
|
let session_id = line_editor
|
|
.get_history_session_id()
|
|
.map(i64::from)
|
|
.unwrap_or(0);
|
|
|
|
engine_state.history_session_id = session_id;
|
|
}
|
|
|
|
fn update_line_editor_history(
|
|
engine_state: &mut EngineState,
|
|
history_path: PathBuf,
|
|
history: HistoryConfig,
|
|
line_editor: Reedline,
|
|
history_session_id: Option<HistorySessionId>,
|
|
) -> Result<Reedline, ErrReport> {
|
|
let history: Box<dyn reedline::History> = match history.file_format {
|
|
HistoryFileFormat::Plaintext => Box::new(
|
|
FileBackedHistory::with_file(history.max_size as usize, history_path)
|
|
.into_diagnostic()?,
|
|
),
|
|
HistoryFileFormat::Sqlite => Box::new(
|
|
SqliteBackedHistory::with_file(
|
|
history_path.to_path_buf(),
|
|
history_session_id,
|
|
Some(chrono::Utc::now()),
|
|
)
|
|
.into_diagnostic()?,
|
|
),
|
|
};
|
|
let line_editor = line_editor
|
|
.with_history_session_id(history_session_id)
|
|
.with_history_exclusion_prefix(Some(" ".into()))
|
|
.with_history(history);
|
|
|
|
store_history_id_in_engine(engine_state, &line_editor);
|
|
|
|
Ok(line_editor)
|
|
}
|
|
|
|
fn confirm_stdin_is_terminal() -> Result<()> {
|
|
// Guard against invocation without a connected terminal.
|
|
// reedline / crossterm event polling will fail without a connected tty
|
|
if !std::io::stdin().is_terminal() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
"Nushell launched as a REPL, but STDIN is not a TTY; either launch in a valid terminal or provide arguments to invoke a script!",
|
|
))
|
|
.into_diagnostic();
|
|
}
|
|
Ok(())
|
|
}
|
|
fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
|
|
match shape {
|
|
NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
|
|
NuCursorShape::Underscore => Some(SetCursorStyle::SteadyUnderScore),
|
|
NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
|
|
NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
|
|
NuCursorShape::BlinkUnderscore => Some(SetCursorStyle::BlinkingUnderScore),
|
|
NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
|
|
NuCursorShape::Inherit => None,
|
|
}
|
|
}
|
|
|
|
fn get_command_finished_marker(
|
|
stack: &Stack,
|
|
engine_state: &EngineState,
|
|
shell_integration_osc633: bool,
|
|
shell_integration_osc133: bool,
|
|
) -> String {
|
|
let exit_code = stack
|
|
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
|
.and_then(|e| e.as_int().ok());
|
|
|
|
if shell_integration_osc633 {
|
|
if stack
|
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
|
.and_then(|v| v.as_str().ok())
|
|
== Some("vscode")
|
|
{
|
|
// We're in vscode and we have osc633 enabled
|
|
format!(
|
|
"{}{}{}",
|
|
VSCODE_POST_EXECUTION_MARKER_PREFIX,
|
|
exit_code.unwrap_or(0),
|
|
VSCODE_POST_EXECUTION_MARKER_SUFFIX
|
|
)
|
|
} else if shell_integration_osc133 {
|
|
// If we're in VSCode but we don't find the env var, just return the regular markers
|
|
format!(
|
|
"{}{}{}",
|
|
POST_EXECUTION_MARKER_PREFIX,
|
|
exit_code.unwrap_or(0),
|
|
POST_EXECUTION_MARKER_SUFFIX
|
|
)
|
|
} else {
|
|
// We're not in vscode, so we don't need to do anything special
|
|
"\x1b[0m".to_string()
|
|
}
|
|
} else if shell_integration_osc133 {
|
|
format!(
|
|
"{}{}{}",
|
|
POST_EXECUTION_MARKER_PREFIX,
|
|
exit_code.unwrap_or(0),
|
|
POST_EXECUTION_MARKER_SUFFIX
|
|
)
|
|
} else {
|
|
"\x1b[0m".to_string()
|
|
}
|
|
}
|
|
|
|
fn run_ansi_sequence(seq: &str) {
|
|
if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
|
|
warn!("Error writing ansi sequence {e}");
|
|
} else if let Err(e) = io::stdout().flush() {
|
|
warn!("Error flushing stdio {e}");
|
|
}
|
|
}
|
|
|
|
fn run_finaliziation_ansi_sequence(
|
|
stack: &Stack,
|
|
engine_state: &EngineState,
|
|
use_color: bool,
|
|
shell_integration_osc633: bool,
|
|
shell_integration_osc133: bool,
|
|
) {
|
|
if shell_integration_osc633 {
|
|
// Only run osc633 if we are in vscode
|
|
if stack
|
|
.get_env_var(engine_state, "TERM_PROGRAM")
|
|
.and_then(|v| v.as_str().ok())
|
|
== Some("vscode")
|
|
{
|
|
let start_time = Instant::now();
|
|
|
|
run_ansi_sequence(&get_command_finished_marker(
|
|
stack,
|
|
engine_state,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
));
|
|
|
|
perf!(
|
|
"post_execute_marker (633;D) ansi escape sequences",
|
|
start_time,
|
|
use_color
|
|
);
|
|
} else if shell_integration_osc133 {
|
|
let start_time = Instant::now();
|
|
|
|
run_ansi_sequence(&get_command_finished_marker(
|
|
stack,
|
|
engine_state,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
));
|
|
|
|
perf!(
|
|
"post_execute_marker (133;D) ansi escape sequences",
|
|
start_time,
|
|
use_color
|
|
);
|
|
}
|
|
} else if shell_integration_osc133 {
|
|
let start_time = Instant::now();
|
|
|
|
run_ansi_sequence(&get_command_finished_marker(
|
|
stack,
|
|
engine_state,
|
|
shell_integration_osc633,
|
|
shell_integration_osc133,
|
|
));
|
|
|
|
perf!(
|
|
"post_execute_marker (133;D) ansi escape sequences",
|
|
start_time,
|
|
use_color
|
|
);
|
|
}
|
|
}
|
|
|
|
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'
|
|
#[cfg(windows)]
|
|
static DRIVE_PATH_REGEX: std::sync::LazyLock<fancy_regex::Regex> = std::sync::LazyLock::new(|| {
|
|
fancy_regex::Regex::new(r"^[a-zA-Z]:[/\\]?").expect("Internal error: regex creation")
|
|
});
|
|
|
|
// A best-effort "does this string look kinda like a path?" function to determine whether to auto-cd
|
|
fn looks_like_path(orig: &str) -> bool {
|
|
#[cfg(windows)]
|
|
{
|
|
if DRIVE_PATH_REGEX.is_match(orig).unwrap_or(false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
orig.starts_with('.')
|
|
|| orig.starts_with('~')
|
|
|| orig.starts_with('/')
|
|
|| orig.starts_with('\\')
|
|
|| orig.ends_with(std::path::MAIN_SEPARATOR)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn looks_like_path_windows_drive_path_works() {
|
|
assert!(looks_like_path("C:"));
|
|
assert!(looks_like_path("D:\\"));
|
|
assert!(looks_like_path("E:/"));
|
|
assert!(looks_like_path("F:\\some_dir"));
|
|
assert!(looks_like_path("G:/some_dir"));
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn trailing_slash_looks_like_path() {
|
|
assert!(looks_like_path("foo\\"))
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[test]
|
|
fn trailing_slash_looks_like_path() {
|
|
assert!(looks_like_path("foo/"))
|
|
}
|
|
|
|
#[test]
|
|
fn are_session_ids_in_sync() {
|
|
let engine_state = &mut EngineState::new();
|
|
let history = engine_state.history_config().unwrap();
|
|
let history_path = history.file_path().unwrap();
|
|
let line_editor = reedline::Reedline::create();
|
|
let history_session_id = reedline::Reedline::create_history_session_id();
|
|
let line_editor = update_line_editor_history(
|
|
engine_state,
|
|
history_path,
|
|
history,
|
|
line_editor,
|
|
history_session_id,
|
|
);
|
|
assert_eq!(
|
|
i64::from(line_editor.unwrap().get_history_session_id().unwrap()),
|
|
engine_state.history_session_id
|
|
);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_auto_cd {
|
|
use super::{do_auto_cd, escape_special_vscode_bytes, parse_operation, ReplOperation};
|
|
use nu_path::AbsolutePath;
|
|
use nu_protocol::engine::{EngineState, Stack};
|
|
use tempfile::tempdir;
|
|
|
|
/// Create a symlink. Works on both Unix and Windows.
|
|
#[cfg(any(unix, windows))]
|
|
fn symlink(
|
|
original: impl AsRef<AbsolutePath>,
|
|
link: impl AsRef<AbsolutePath>,
|
|
) -> std::io::Result<()> {
|
|
let original = original.as_ref();
|
|
let link = link.as_ref();
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
std::os::unix::fs::symlink(original, link)
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
if original.is_dir() {
|
|
std::os::windows::fs::symlink_dir(original, link)
|
|
} else {
|
|
std::os::windows::fs::symlink_file(original, link)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run one test case on the auto-cd feature. PWD is initially set to
|
|
/// `before`, and after `input` is parsed and evaluated, PWD should be
|
|
/// changed to `after`.
|
|
#[track_caller]
|
|
fn check(before: impl AsRef<AbsolutePath>, input: &str, after: impl AsRef<AbsolutePath>) {
|
|
// Setup EngineState and Stack.
|
|
let mut engine_state = EngineState::new();
|
|
let mut stack = Stack::new();
|
|
stack.set_cwd(before.as_ref()).unwrap();
|
|
|
|
// Parse the input. It must be an auto-cd operation.
|
|
let op = parse_operation(input.to_string(), &engine_state, &stack).unwrap();
|
|
let ReplOperation::AutoCd { cwd, target, span } = op else {
|
|
panic!("'{}' was not parsed into an auto-cd operation", input)
|
|
};
|
|
|
|
// Perform the auto-cd operation.
|
|
do_auto_cd(target, cwd, &mut stack, &mut engine_state, span);
|
|
let updated_cwd = engine_state.cwd(Some(&stack)).unwrap();
|
|
|
|
// Check that `updated_cwd` and `after` point to the same place. They
|
|
// don't have to be byte-wise equal (on Windows, the 8.3 filename
|
|
// conversion messes things up),
|
|
let updated_cwd = std::fs::canonicalize(updated_cwd).unwrap();
|
|
let after = std::fs::canonicalize(after.as_ref()).unwrap();
|
|
assert_eq!(updated_cwd, after);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_root() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let input = if cfg!(windows) { r"C:\" } else { "/" };
|
|
let root = AbsolutePath::try_new(input).unwrap();
|
|
check(tempdir, input, root);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_tilde() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let home = nu_path::home_dir().unwrap();
|
|
check(tempdir, "~", home);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_dot() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
check(tempdir, ".", tempdir);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_double_dot() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let dir = tempdir.join("foo");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
check(dir, "..", tempdir);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_triple_dot() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let dir = tempdir.join("foo").join("bar");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
check(dir, "...", tempdir);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_relative() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let foo = tempdir.join("foo");
|
|
let bar = tempdir.join("bar");
|
|
std::fs::create_dir_all(&foo).unwrap();
|
|
std::fs::create_dir_all(&bar).unwrap();
|
|
let input = if cfg!(windows) { r"..\bar" } else { "../bar" };
|
|
check(foo, input, bar);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_trailing_slash() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let dir = tempdir.join("foo");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
|
|
check(tempdir, input, dir);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_cd_symlink() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let dir = tempdir.join("foo");
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let link = tempdir.join("link");
|
|
symlink(&dir, &link).unwrap();
|
|
let input = if cfg!(windows) { r".\link" } else { "./link" };
|
|
check(tempdir, input, link);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "was not parsed into an auto-cd operation")]
|
|
fn auto_cd_nonexistent_directory() {
|
|
let tempdir = tempdir().unwrap();
|
|
let tempdir = AbsolutePath::try_new(tempdir.path()).unwrap();
|
|
|
|
let dir = tempdir.join("foo");
|
|
let input = if cfg!(windows) { r"foo\" } else { "foo/" };
|
|
check(tempdir, input, dir);
|
|
}
|
|
|
|
#[test]
|
|
fn escape_vscode_semicolon_test() {
|
|
let input = r#"now;is"#;
|
|
let expected = r#"now\x3Bis"#;
|
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
|
assert_eq!(expected, actual);
|
|
}
|
|
|
|
#[test]
|
|
fn escape_vscode_backslash_test() {
|
|
let input = r#"now\is"#;
|
|
let expected = r#"now\\is"#;
|
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
|
assert_eq!(expected, actual);
|
|
}
|
|
|
|
#[test]
|
|
fn escape_vscode_linefeed_test() {
|
|
let input = "now\nis";
|
|
let expected = r#"now\x0Ais"#;
|
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
|
assert_eq!(expected, actual);
|
|
}
|
|
|
|
#[test]
|
|
fn escape_vscode_tab_null_cr_test() {
|
|
let input = "now\t\0\ris";
|
|
let expected = r#"now\x09\x00\x0Dis"#;
|
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
|
assert_eq!(expected, actual);
|
|
}
|
|
|
|
#[test]
|
|
fn escape_vscode_multibyte_ok() {
|
|
let input = "now🍪is";
|
|
let actual = escape_special_vscode_bytes(input).unwrap();
|
|
assert_eq!(input, actual);
|
|
}
|
|
}
|