mirror of
https://github.com/nushell/nushell.git
synced 2025-04-27 14:48:21 +02:00
Tidy up the REPL main loop (#11655)
While working on #11288 I was having some trouble tracking the main REPL loop, so I've sent in a bunch of tiny refactorings on this branch. These are almost all of the "move code from one place to another" variety, and each commit is meant to be independent, _except for the last one_, which is trying to be a bit more clever to handle the decision of autocd'ing vs running a command. Feel free to just go through each commit and cherry pick the ones that look good. This leads to `evaluate_repl` going from ending on line 715 to ending on line 395. Again, this is mostly just moving code around, but I think this set of changes will make other changes around juggling the stack to avoid cloning easier to review.
This commit is contained in:
parent
d9b324c5d3
commit
d40019a141
@ -17,8 +17,8 @@ use nu_protocol::{
|
|||||||
config::NuCursorShape,
|
config::NuCursorShape,
|
||||||
engine::{EngineState, Stack, StateWorkingSet},
|
engine::{EngineState, Stack, StateWorkingSet},
|
||||||
eval_const::create_nu_constant,
|
eval_const::create_nu_constant,
|
||||||
report_error, report_error_new, HistoryConfig, HistoryFileFormat, PipelineData, ShellError,
|
report_error_new, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned,
|
||||||
Span, Spanned, Value, NU_VARIABLE_ID,
|
Value, NU_VARIABLE_ID,
|
||||||
};
|
};
|
||||||
use nu_utils::utils::perf;
|
use nu_utils::utils::perf;
|
||||||
use reedline::{
|
use reedline::{
|
||||||
@ -30,7 +30,7 @@ use std::{
|
|||||||
io::{self, IsTerminal, Write},
|
io::{self, IsTerminal, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::atomic::Ordering,
|
sync::atomic::Ordering,
|
||||||
time::Instant,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
|
|
||||||
@ -44,6 +44,9 @@ const PRE_EXECUTE_MARKER: &str = "\x1b]133;C\x1b\\";
|
|||||||
// const CMD_FINISHED_MARKER: &str = "\x1b]133;D;{}\x1b\\";
|
// const CMD_FINISHED_MARKER: &str = "\x1b]133;D;{}\x1b\\";
|
||||||
const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
|
const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
|
||||||
|
|
||||||
|
///
|
||||||
|
/// The main REPL loop, including spinning up the prompt itself.
|
||||||
|
///
|
||||||
pub fn evaluate_repl(
|
pub fn evaluate_repl(
|
||||||
engine_state: &mut EngineState,
|
engine_state: &mut EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
@ -57,15 +60,7 @@ pub fn evaluate_repl(
|
|||||||
let config = engine_state.get_config();
|
let config = engine_state.get_config();
|
||||||
let use_color = config.use_ansi_coloring;
|
let use_color = config.use_ansi_coloring;
|
||||||
|
|
||||||
// Guard against invocation without a connected terminal.
|
confirm_stdin_is_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();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entry_num = 0;
|
let mut entry_num = 0;
|
||||||
|
|
||||||
@ -74,8 +69,7 @@ pub fn evaluate_repl(
|
|||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
// Translate environment variables from Strings to Values
|
// Translate environment variables from Strings to Values
|
||||||
if let Some(e) = convert_env_values(engine_state, stack) {
|
if let Some(e) = convert_env_values(engine_state, stack) {
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
report_error_new(engine_state, &e);
|
||||||
report_error(&working_set, &e);
|
|
||||||
}
|
}
|
||||||
perf(
|
perf(
|
||||||
"translate env vars",
|
"translate env vars",
|
||||||
@ -112,23 +106,7 @@ pub fn evaluate_repl(
|
|||||||
if let Some(history) = engine_state.history_config() {
|
if let Some(history) = engine_state.history_config() {
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
|
|
||||||
// Setup history_isolation aka "history per session"
|
line_editor = setup_history(nushell_path, engine_state, line_editor, history)?;
|
||||||
let history_session_id = if history.isolation {
|
|
||||||
Reedline::create_history_session_id()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(path) = crate::config_files::get_history_path(nushell_path, history.file_format)
|
|
||||||
{
|
|
||||||
line_editor = update_line_editor_history(
|
|
||||||
engine_state,
|
|
||||||
path,
|
|
||||||
history,
|
|
||||||
line_editor,
|
|
||||||
history_session_id,
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
|
|
||||||
perf(
|
perf(
|
||||||
"setup history",
|
"setup history",
|
||||||
@ -169,9 +147,7 @@ pub fn evaluate_repl(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if engine_state.get_config().use_kitty_protocol && !reedline::kitty_protocol_available() {
|
kitty_protocol_healthcheck(engine_state);
|
||||||
warn!("Terminal doesn't support use_kitty_protocol config");
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let loop_start_time = std::time::Instant::now();
|
let loop_start_time = std::time::Instant::now();
|
||||||
@ -282,8 +258,7 @@ pub fn evaluate_repl(
|
|||||||
|
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
line_editor = add_menus(line_editor, engine_reference, stack, config).unwrap_or_else(|e| {
|
line_editor = add_menus(line_editor, engine_reference, stack, config).unwrap_or_else(|e| {
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
report_error_new(engine_state, &e);
|
||||||
report_error(&working_set, &e);
|
|
||||||
Reedline::create()
|
Reedline::create()
|
||||||
});
|
});
|
||||||
perf(
|
perf(
|
||||||
@ -335,26 +310,7 @@ pub fn evaluate_repl(
|
|||||||
|
|
||||||
start_time = std::time::Instant::now();
|
start_time = std::time::Instant::now();
|
||||||
// Changing the line editor based on the found keybindings
|
// Changing the line editor based on the found keybindings
|
||||||
line_editor = match create_keybindings(config) {
|
line_editor = setup_keybindings(engine_state, line_editor);
|
||||||
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) => {
|
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
|
||||||
report_error(&working_set, &e);
|
|
||||||
line_editor
|
|
||||||
}
|
|
||||||
};
|
|
||||||
perf(
|
perf(
|
||||||
"keybindings",
|
"keybindings",
|
||||||
start_time,
|
start_time,
|
||||||
@ -427,17 +383,9 @@ pub fn evaluate_repl(
|
|||||||
engine_state.history_config().map(|h| h.file_format),
|
engine_state.history_config().map(|h| h.file_format),
|
||||||
Some(HistoryFileFormat::Sqlite)
|
Some(HistoryFileFormat::Sqlite)
|
||||||
);
|
);
|
||||||
if history_supports_meta && !s.is_empty() && line_editor.has_last_command_context()
|
|
||||||
{
|
|
||||||
line_editor
|
|
||||||
.update_last_command_context(&|mut c| {
|
|
||||||
c.start_timestamp = Some(chrono::Utc::now());
|
|
||||||
c.hostname = hostname.clone();
|
|
||||||
|
|
||||||
c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd());
|
if history_supports_meta {
|
||||||
c
|
prepare_history_metadata(&s, &hostname, engine_state, &mut line_editor)?;
|
||||||
})
|
|
||||||
.into_diagnostic()?; // todo: don't stop repl if error here?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right before we start running the code the user gave us, fire the `pre_execution`
|
// Right before we start running the code the user gave us, fire the `pre_execution`
|
||||||
@ -464,127 +412,27 @@ pub fn evaluate_repl(
|
|||||||
run_ansi_sequence(PRE_EXECUTE_MARKER)?;
|
run_ansi_sequence(PRE_EXECUTE_MARKER)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Actual command execution logic starts from here
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let tokens = lex(s.as_bytes(), 0, &[], &[], false);
|
|
||||||
// Check if this is a single call to a directory, if so auto-cd
|
|
||||||
let cwd = nu_engine::env::current_dir_str(engine_state, stack)?;
|
|
||||||
|
|
||||||
let mut orig = s.clone();
|
match parse_operation(s.clone(), engine_state, stack)? {
|
||||||
if orig.starts_with('`') {
|
ReplOperation::AutoCd { cwd, target, span } => {
|
||||||
orig = trim_quotes_str(&orig).to_string()
|
do_auto_cd(target, cwd, stack, engine_state, span);
|
||||||
}
|
}
|
||||||
|
ReplOperation::RunCommand(cmd) => {
|
||||||
let path = nu_path::expand_path_with(&orig, &cwd);
|
line_editor = do_run_cmd(
|
||||||
|
&cmd,
|
||||||
if looks_like_path(&orig) && path.is_dir() && tokens.0.len() == 1 {
|
|
||||||
// We have an auto-cd
|
|
||||||
let (path, span) = {
|
|
||||||
if !path.exists() {
|
|
||||||
let working_set = StateWorkingSet::new(engine_state);
|
|
||||||
|
|
||||||
report_error(
|
|
||||||
&working_set,
|
|
||||||
&ShellError::DirectoryNotFound {
|
|
||||||
dir: path.to_string_lossy().to_string(),
|
|
||||||
span: tokens.0[0].span,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let path = nu_path::canonicalize_with(path, &cwd)
|
|
||||||
.expect("internal error: cannot canonicalize known path");
|
|
||||||
(path.to_string_lossy().to_string(), tokens.0[0].span)
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
stack.add_env_var("PWD".into(), Value::string(path.clone(), Span::unknown()));
|
|
||||||
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.as_list()
|
|
||||||
.map(|x| x.to_vec())
|
|
||||||
.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),
|
|
||||||
);
|
|
||||||
} else if !s.trim().is_empty() {
|
|
||||||
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 {
|
|
||||||
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
|
|
||||||
let path = cwd.as_string()?;
|
|
||||||
|
|
||||||
// Try to abbreviate string for windows title
|
|
||||||
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
|
|
||||||
path.replace(&p.as_path().display().to_string(), "~")
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
};
|
|
||||||
let binary_name = s.split_whitespace().next();
|
|
||||||
|
|
||||||
if let Some(binary_name) = binary_name {
|
|
||||||
run_ansi_sequence(&format!(
|
|
||||||
"\x1b]2;{maybe_abbrev_path}> {binary_name}\x07"
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eval_source(
|
|
||||||
engine_state,
|
|
||||||
stack,
|
stack,
|
||||||
s.as_bytes(),
|
engine_state,
|
||||||
&format!("entry #{entry_num}"),
|
line_editor,
|
||||||
PipelineData::empty(),
|
shell_integration,
|
||||||
false,
|
entry_num,
|
||||||
);
|
)?;
|
||||||
}
|
}
|
||||||
|
// as the name implies, we do nothing in this case
|
||||||
|
ReplOperation::DoNothing => {}
|
||||||
|
}
|
||||||
|
|
||||||
let cmd_duration = start_time.elapsed();
|
let cmd_duration = start_time.elapsed();
|
||||||
|
|
||||||
stack.add_env_var(
|
stack.add_env_var(
|
||||||
@ -592,77 +440,21 @@ pub fn evaluate_repl(
|
|||||||
Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
|
Value::string(format!("{}", cmd_duration.as_millis()), Span::unknown()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if history_supports_meta && !s.is_empty() && line_editor.has_last_command_context()
|
if history_supports_meta {
|
||||||
{
|
fill_in_result_related_history_metadata(
|
||||||
line_editor
|
&s,
|
||||||
.update_last_command_context(&|mut c| {
|
engine_state,
|
||||||
c.duration = Some(cmd_duration);
|
cmd_duration,
|
||||||
c.exit_status = stack
|
stack,
|
||||||
.get_env_var(engine_state, "LAST_EXIT_CODE")
|
&mut line_editor,
|
||||||
.and_then(|e| e.as_i64().ok());
|
)?;
|
||||||
c
|
|
||||||
})
|
|
||||||
.into_diagnostic()?; // todo: don't stop repl if error here?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if shell_integration {
|
if shell_integration {
|
||||||
run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
|
do_shell_integration_finalize_command(hostname, engine_state, stack)?;
|
||||||
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
|
|
||||||
let path = cwd.as_string()?;
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
== Some(Value::test_string("vscode"))
|
|
||||||
{
|
|
||||||
// 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!("\x1b]633;P;Cwd={}\x1b\\", path))?;
|
|
||||||
} else {
|
|
||||||
// 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_else(|| "localhost".to_string()),
|
|
||||||
percent_encoding::CONTROLS
|
|
||||||
),
|
|
||||||
if path.starts_with('/') { "" } else { "/" },
|
|
||||||
percent_encoding::utf8_percent_encode(
|
|
||||||
&path,
|
|
||||||
percent_encoding::CONTROLS
|
|
||||||
)
|
|
||||||
))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to abbreviate string for windows title
|
flush_engine_state_repl_buffer(engine_state, &mut line_editor);
|
||||||
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
|
|
||||||
path.replace(&p.as_path().display().to_string(), "~")
|
|
||||||
} else {
|
|
||||||
path
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;{maybe_abbrev_path}\x07"))?;
|
|
||||||
}
|
|
||||||
run_ansi_sequence(RESET_APPLICATION_MODE)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
drop(repl);
|
|
||||||
}
|
}
|
||||||
Ok(Signal::CtrlC) => {
|
Ok(Signal::CtrlC) => {
|
||||||
// `Reedline` clears the line content. New prompt is shown
|
// `Reedline` clears the line content. New prompt is shown
|
||||||
@ -714,6 +506,364 @@ pub fn evaluate_repl(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Put in history metadata not related to the result of running the command
|
||||||
|
///
|
||||||
|
fn prepare_history_metadata(
|
||||||
|
s: &str,
|
||||||
|
hostname: &Option<String>,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
line_editor: &mut Reedline,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !s.is_empty() && line_editor.has_last_command_context() {
|
||||||
|
line_editor
|
||||||
|
.update_last_command_context(&|mut c| {
|
||||||
|
c.start_timestamp = Some(chrono::Utc::now());
|
||||||
|
c.hostname = hostname.clone();
|
||||||
|
|
||||||
|
c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd());
|
||||||
|
c
|
||||||
|
})
|
||||||
|
.into_diagnostic()?; // todo: don't stop repl if error here?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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_i64().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
|
||||||
|
let cwd = nu_engine::env::current_dir_str(engine_state, stack)?;
|
||||||
|
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);
|
||||||
|
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_error_new(
|
||||||
|
engine_state,
|
||||||
|
&ShellError::DirectoryNotFound {
|
||||||
|
dir: path.to_string_lossy().to_string(),
|
||||||
|
span,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let path = nu_path::canonicalize_with(path, &cwd)
|
||||||
|
.expect("internal error: cannot canonicalize known path");
|
||||||
|
path.to_string_lossy().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
stack.add_env_var("PWD".into(), Value::string(path.clone(), Span::unknown()));
|
||||||
|
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.as_list()
|
||||||
|
.map(|x| x.to_vec())
|
||||||
|
.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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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: bool,
|
||||||
|
entry_num: usize,
|
||||||
|
) -> Result<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 {
|
||||||
|
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
|
||||||
|
let path = cwd.as_string()?;
|
||||||
|
|
||||||
|
// Try to abbreviate string for windows title
|
||||||
|
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
|
||||||
|
path.replace(&p.as_path().display().to_string(), "~")
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
let binary_name = s.split_whitespace().next();
|
||||||
|
|
||||||
|
if let Some(binary_name) = binary_name {
|
||||||
|
run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}> {binary_name}\x07"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eval_source(
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
s.as_bytes(),
|
||||||
|
&format!("entry #{entry_num}"),
|
||||||
|
PipelineData::empty(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(line_editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Output some things and set environment variables so shells with the right integration
|
||||||
|
/// can have more information about what is going on (after we have run a command)
|
||||||
|
///
|
||||||
|
fn do_shell_integration_finalize_command(
|
||||||
|
hostname: Option<String>,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
) -> Result<()> {
|
||||||
|
run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?;
|
||||||
|
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
|
||||||
|
let path = cwd.as_string()?;
|
||||||
|
|
||||||
|
// 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") == Some(Value::test_string("vscode")) {
|
||||||
|
// 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!("\x1b]633;P;Cwd={}\x1b\\", path))?;
|
||||||
|
} else {
|
||||||
|
// 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_else(|| "localhost".to_string()),
|
||||||
|
percent_encoding::CONTROLS
|
||||||
|
),
|
||||||
|
if path.starts_with('/') { "" } else { "/" },
|
||||||
|
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to abbreviate string for windows title
|
||||||
|
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
|
||||||
|
path.replace(&p.as_path().display().to_string(), "~")
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;{maybe_abbrev_path}\x07"))?;
|
||||||
|
}
|
||||||
|
run_ansi_sequence(RESET_APPLICATION_MODE)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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(
|
||||||
|
nushell_path: &str,
|
||||||
|
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) = crate::config_files::get_history_path(nushell_path, history.file_format) {
|
||||||
|
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_error_new(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) {
|
fn store_history_id_in_engine(engine_state: &mut EngineState, line_editor: &Reedline) {
|
||||||
let session_id = line_editor
|
let session_id = line_editor
|
||||||
.get_history_session_id()
|
.get_history_session_id()
|
||||||
@ -754,6 +904,18 @@ fn update_line_editor_history(
|
|||||||
Ok(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> {
|
fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
|
||||||
match shape {
|
match shape {
|
||||||
NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
|
NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
|
||||||
|
Loading…
Reference in New Issue
Block a user