Allow for stacks to have parents (#11654)

This is another attempt on #11288 

This allows for a `Stack` to have a parent stack (behind an `Arc`). This
is being added to avoid constant stack copying in REPL code.

Concretely the following changes are included here:
- `Stack` can now have a `parent_stack`, pointing to another stack
- variable lookups can fallback to this parent stack (env vars and
everything else is still copied)
- REPL code has been reworked so that we use parenting rather than
cloning. A REPL-code-specific trait helps to ensure that we do not
accidentally trigger a full clone of the main stack
- A property test has been added to make sure that parenting "looks the
same" as cloning for consumers of `Stack` objects

---------

Co-authored-by: Raphael Gaschignard <rtpg@rokkenjima.local>
Co-authored-by: Ian Manske <ian.manske@pm.me>
This commit is contained in:
Raphael Gaschignard
2024-03-10 01:55:39 +09:00
committed by GitHub
parent c90640411d
commit d8f13b36b1
7 changed files with 338 additions and 87 deletions

View File

@ -1,7 +1,7 @@
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Type, Value};
use reedline::Highlighter;
use reedline::{Highlighter, StyledText};
#[derive(Clone)]
pub struct NuHighlight;
@ -64,3 +64,16 @@ impl Command for NuHighlight {
}]
}
}
/// A highlighter that does nothing
///
/// Used to remove highlighting from a reedline instance
/// (letting NuHighlighter structs be dropped)
#[derive(Default)]
pub struct NoOpHighlighter {}
impl Highlighter for NoOpHighlighter {
fn highlight(&self, _line: &str, _cursor: usize) -> reedline::StyledText {
StyledText::new()
}
}

View File

@ -102,12 +102,10 @@ fn get_prompt_string(
pub(crate) fn update_prompt(
config: &Config,
engine_state: &EngineState,
stack: &Stack,
stack: &mut Stack,
nu_prompt: &mut NushellPrompt,
) {
let mut stack = stack.clone();
let left_prompt_string = get_prompt_string(PROMPT_COMMAND, config, engine_state, &mut stack);
let left_prompt_string = get_prompt_string(PROMPT_COMMAND, config, engine_state, stack);
// Now that we have the prompt string lets ansify it.
// <133 A><prompt><133 B><command><133 C><command output>
@ -123,20 +121,18 @@ pub(crate) fn update_prompt(
left_prompt_string
};
let right_prompt_string =
get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, &mut stack);
let right_prompt_string = get_prompt_string(PROMPT_COMMAND_RIGHT, config, engine_state, stack);
let prompt_indicator_string =
get_prompt_string(PROMPT_INDICATOR, config, engine_state, &mut stack);
let prompt_indicator_string = get_prompt_string(PROMPT_INDICATOR, config, engine_state, stack);
let prompt_multiline_string =
get_prompt_string(PROMPT_MULTILINE_INDICATOR, config, engine_state, &mut stack);
get_prompt_string(PROMPT_MULTILINE_INDICATOR, config, engine_state, stack);
let prompt_vi_insert_string =
get_prompt_string(PROMPT_INDICATOR_VI_INSERT, config, engine_state, &mut stack);
get_prompt_string(PROMPT_INDICATOR_VI_INSERT, config, engine_state, stack);
let prompt_vi_normal_string =
get_prompt_string(PROMPT_INDICATOR_VI_NORMAL, config, engine_state, &mut stack);
get_prompt_string(PROMPT_INDICATOR_VI_NORMAL, config, engine_state, stack);
// apply the other indicators
nu_prompt.update_all_prompt_strings(

View File

@ -1,5 +1,6 @@
use crate::{
completions::NuCompleter,
nu_highlight::NoOpHighlighter,
prompt_update,
reedline_config::{add_menus, create_keybindings, KeybindingsMode},
util::eval_source,
@ -22,8 +23,8 @@ use nu_protocol::{
};
use nu_utils::utils::perf;
use reedline::{
CursorConfig, CwdAwareHinter, EditCommand, Emacs, FileBackedHistory, HistorySessionId,
Reedline, SqliteBackedHistory, Vi,
CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory,
HistorySessionId, Reedline, SqliteBackedHistory, Vi,
};
use std::{
collections::HashMap,
@ -32,7 +33,7 @@ use std::{
panic::{catch_unwind, AssertUnwindSafe},
path::Path,
path::PathBuf,
sync::atomic::Ordering,
sync::{atomic::Ordering, Arc},
time::{Duration, Instant},
};
use sysinfo::System;
@ -47,17 +48,21 @@ const PRE_EXECUTE_MARKER: &str = "\x1b]133;C\x1b\\";
// const CMD_FINISHED_MARKER: &str = "\x1b]133;D;{}\x1b\\";
const RESET_APPLICATION_MODE: &str = "\x1b[?1l";
///
/// The main REPL loop, including spinning up the prompt itself.
///
pub fn evaluate_repl(
engine_state: &mut EngineState,
stack: &mut Stack,
stack: Stack,
nushell_path: &str,
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;
let config = engine_state.get_config();
let use_color = config.use_ansi_coloring;
@ -69,7 +74,7 @@ pub fn evaluate_repl(
let start_time = std::time::Instant::now();
// 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, &unique_stack) {
report_error_new(engine_state, &e);
}
perf(
@ -82,12 +87,12 @@ pub fn evaluate_repl(
);
// seed env vars
stack.add_env_var(
unique_stack.add_env_var(
"CMD_DURATION_MS".into(),
Value::string("0823", Span::unknown()),
);
stack.add_env_var("LAST_EXIT_CODE".into(), Value::int(0, Span::unknown()));
unique_stack.add_env_var("LAST_EXIT_CODE".into(), Value::int(0, Span::unknown()));
let mut line_editor = get_line_editor(engine_state, nushell_path, use_color)?;
let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
@ -95,13 +100,14 @@ pub fn evaluate_repl(
if let Some(s) = prerun_command {
eval_source(
engine_state,
stack,
&mut unique_stack,
s.item.as_bytes(),
&format!("entry #{entry_num}"),
PipelineData::empty(),
false,
);
engine_state.merge_env(stack, get_guaranteed_cwd(engine_state, stack))?;
let cwd = get_guaranteed_cwd(engine_state, &unique_stack);
engine_state.merge_env(&mut unique_stack, cwd)?;
}
engine_state.set_startup_time(entire_start_time.elapsed().as_nanos() as i64);
@ -113,7 +119,7 @@ pub fn evaluate_repl(
if load_std_lib.is_none() && engine_state.get_config().show_banner {
eval_source(
engine_state,
stack,
&mut unique_stack,
r#"use std banner; banner"#.as_bytes(),
"show_banner",
PipelineData::empty(),
@ -125,20 +131,22 @@ pub fn evaluate_repl(
// Setup initial engine_state and stack state
let mut previous_engine_state = engine_state.clone();
let mut previous_stack = stack.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();
let mut current_stack = previous_stack.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();
match catch_unwind(AssertUnwindSafe(move || {
let (continue_loop, line_editor) = loop_iteration(LoopContext {
let (continue_loop, current_stack, line_editor) = loop_iteration(LoopContext {
engine_state: &mut current_engine_state,
stack: &mut current_stack,
stack: current_stack,
line_editor,
nu_prompt: &mut nu_prompt_cloned,
temp_file: &temp_file_cloned,
@ -157,7 +165,9 @@ pub fn evaluate_repl(
Ok((continue_loop, es, s, le)) => {
// setup state for the next iteration of the repl loop
previous_engine_state = es;
previous_stack = s;
// 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;
@ -211,7 +221,7 @@ fn get_line_editor(
struct LoopContext<'a> {
engine_state: &'a mut EngineState,
stack: &'a mut Stack,
stack: Stack,
line_editor: Reedline,
nu_prompt: &'a mut NushellPrompt,
temp_file: &'a Path,
@ -222,14 +232,14 @@ struct LoopContext<'a> {
/// Perform one iteration of the REPL loop
/// Result is bool: continue loop, current reedline
#[inline]
fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
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,
stack,
mut stack,
line_editor,
nu_prompt,
temp_file,
@ -237,12 +247,12 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
entry_num,
} = ctx;
let cwd = get_guaranteed_cwd(engine_state, stack);
let cwd = get_guaranteed_cwd(engine_state, &stack);
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(stack, cwd) {
if let Err(err) = engine_state.merge_env(&mut stack, cwd) {
report_error_new(engine_state, &err);
}
perf(
@ -289,6 +299,10 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
);
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 mut stack_arc = Arc::new(stack);
let mut line_editor = line_editor
.use_kitty_keyboard_enhancement(config.use_kitty_protocol)
@ -297,7 +311,8 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
.use_bracketed_paste(cfg!(not(target_os = "windows")) && config.bracketed_paste)
.with_highlighter(Box::new(NuHighlighter {
engine_state: engine_reference.clone(),
stack: std::sync::Arc::new(stack.clone()),
// STACK-REFERENCE 1
stack: stack_arc.clone(),
config: config.clone(),
}))
.with_validator(Box::new(NuValidator {
@ -305,7 +320,8 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
}))
.with_completer(Box::new(NuCompleter::new(
engine_reference.clone(),
stack.clone(),
// STACK-REFERENCE 2
Stack::with_parent(stack_arc.clone()),
)))
.with_quick_completions(config.quick_completions)
.with_partial_completions(config.partial_completions)
@ -320,7 +336,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
use_color,
);
let style_computer = StyleComputer::from_config(engine_state, stack);
let style_computer = StyleComputer::from_config(engine_state, &stack_arc);
start_time = std::time::Instant::now();
line_editor = if config.use_ansi_coloring {
@ -342,10 +358,11 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
);
start_time = std::time::Instant::now();
line_editor = add_menus(line_editor, engine_reference, stack, config).unwrap_or_else(|e| {
report_error_new(engine_state, &e);
Reedline::create()
});
line_editor =
add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
report_error_new(engine_state, &e);
Reedline::create()
});
perf(
"reedline menus",
start_time,
@ -356,11 +373,11 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
);
start_time = std::time::Instant::now();
let buffer_editor = get_editor(engine_state, stack, Span::unknown());
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).unwrap_or_else(|e| {
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()
});
@ -411,7 +428,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
// Right before we start our prompt and take input from the user,
// fire the "pre_prompt" hook
if let Some(hook) = config.hooks.pre_prompt.clone() {
if let Err(err) = eval_hook(engine_state, stack, None, vec![], &hook, "pre_prompt") {
if let Err(err) = eval_hook(
engine_state,
&mut Stack::with_parent(stack_arc.clone()),
None,
vec![],
&hook,
"pre_prompt",
) {
report_error_new(engine_state, &err);
}
}
@ -428,9 +452,11 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
// Next, check all the environment variables they ask for
// fire the "env_change" hook
let config = engine_state.get_config();
if let Err(error) =
hook::eval_env_change_hook(config.hooks.env_change.clone(), engine_state, stack)
{
if let Err(error) = hook::eval_env_change_hook(
config.hooks.env_change.clone(),
engine_state,
&mut Stack::with_parent(stack_arc.clone()),
) {
report_error_new(engine_state, &error)
}
perf(
@ -444,9 +470,18 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
start_time = std::time::Instant::now();
let config = &engine_state.get_config().clone();
prompt_update::update_prompt(config, engine_state, stack, nu_prompt);
let transient_prompt =
prompt_update::make_transient_prompt(config, engine_state, stack, nu_prompt);
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,
@ -461,6 +496,13 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
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 shell_integration = config.shell_integration;
match input {
@ -483,9 +525,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
repl.buffer = s.to_string();
drop(repl);
if let Err(err) =
eval_hook(engine_state, stack, None, vec![], &hook, "pre_execution")
{
if let Err(err) = eval_hook(
engine_state,
&mut Stack::with_parent(stack_arc.clone()),
None,
vec![],
&hook,
"pre_execution",
) {
report_error_new(engine_state, &err);
}
}
@ -502,15 +549,17 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
// Actual command execution logic starts from here
let start_time = Instant::now();
match parse_operation(s.clone(), engine_state, stack) {
let mut stack = Stack::unwrap_unique(stack_arc);
match parse_operation(s.clone(), engine_state, &stack) {
Ok(operation) => match operation {
ReplOperation::AutoCd { cwd, target, span } => {
do_auto_cd(target, cwd, stack, engine_state, span);
do_auto_cd(target, cwd, &mut stack, engine_state, span);
}
ReplOperation::RunCommand(cmd) => {
line_editor = do_run_cmd(
&cmd,
stack,
&mut stack,
engine_state,
line_editor,
shell_integration,
@ -522,7 +571,6 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
},
Err(ref e) => error!("Error parsing operation: {e}"),
}
let cmd_duration = start_time.elapsed();
stack.add_env_var(
@ -535,7 +583,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
&s,
engine_state,
cmd_duration,
stack,
&mut stack,
&mut line_editor,
) {
warn!("Could not fill in result related history metadata: {e}");
@ -543,24 +591,26 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
}
if shell_integration {
do_shell_integration_finalize_command(hostname, engine_state, stack);
do_shell_integration_finalize_command(hostname, engine_state, &mut stack);
}
flush_engine_state_repl_buffer(engine_state, &mut line_editor);
// put the stack back into the arc
stack_arc = Arc::new(stack);
}
Ok(Signal::CtrlC) => {
// `Reedline` clears the line content. New prompt is shown
if shell_integration {
run_ansi_sequence(&get_command_finished_marker(stack, engine_state));
run_ansi_sequence(&get_command_finished_marker(&stack_arc, engine_state));
}
}
Ok(Signal::CtrlD) => {
// When exiting clear to a new line
if shell_integration {
run_ansi_sequence(&get_command_finished_marker(stack, engine_state));
run_ansi_sequence(&get_command_finished_marker(&stack_arc, engine_state));
}
println!();
return (false, line_editor);
return (false, Stack::unwrap_unique(stack_arc), line_editor);
}
Err(err) => {
let message = err.to_string();
@ -572,7 +622,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
// Alternatively only allow that expected failures let the REPL loop
}
if shell_integration {
run_ansi_sequence(&get_command_finished_marker(stack, engine_state));
run_ansi_sequence(&get_command_finished_marker(&stack_arc, engine_state));
}
}
}
@ -594,7 +644,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
use_color,
);
(true, line_editor)
(true, Stack::unwrap_unique(stack_arc), line_editor)
}
///