From 3a35bf7d4ed1daef2c9beefcf7838e07a1546491 Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Mon, 9 May 2022 07:28:39 +1200 Subject: [PATCH] Add hooks to cli/repl (#5479) * Add hooks to cli/repl * Clippy * Clippy --- crates/nu-cli/src/repl.rs | 55 +++++++++++++++- crates/nu-cli/tests/test_completions.rs | 2 +- crates/nu-command/src/filesystem/cd_query.rs | 4 +- crates/nu-glob/src/lib.rs | 2 +- crates/nu-protocol/src/config.rs | 68 ++++++++++++++++++++ docs/sample_config/default_config.nu | 8 +++ 6 files changed, 134 insertions(+), 5 deletions(-) diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index f798cedd91..f8f2ae8798 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -8,7 +8,7 @@ use crate::{ use log::{info, trace}; use miette::{IntoDiagnostic, Result}; use nu_color_config::get_color_config; -use nu_engine::convert_env_values; +use nu_engine::{convert_env_values, eval_block}; use nu_parser::lex; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, @@ -211,11 +211,29 @@ pub fn evaluate_repl( ); } + // 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 { + if let Err(err) = run_hook(engine_state, stack, hook) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &err); + } + } + let input = line_editor.read_line(prompt); let use_shell_integration = config.shell_integration; match input { Ok(Signal::Success(s)) => { + // Right before we start running the code the user gave us, + // fire the "pre_execution" hook + if let Some(hook) = &config.hooks.pre_execution { + if let Err(err) = run_hook(engine_state, stack, hook) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &err); + } + } + 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 @@ -359,3 +377,38 @@ pub fn evaluate_repl( Ok(()) } + +pub fn run_hook( + engine_state: &EngineState, + stack: &mut Stack, + value: &Value, +) -> Result<(), ShellError> { + match value { + Value::Block { + val: block_id, + span, + .. + } => { + let block = engine_state.get_block(*block_id); + let input = PipelineData::new(*span); + + match eval_block(engine_state, stack, block, input, false, false) { + Ok(pipeline_data) => match pipeline_data.into_value(*span) { + Value::Error { error } => Err(error), + _ => Ok(()), + }, + Err(err) => Err(err), + } + } + x => match x.span() { + Ok(span) => Err(ShellError::MissingConfigValue( + "block for hook in config".into(), + span, + )), + _ => Err(ShellError::MissingConfigValue( + "block for hook in config".into(), + Span { start: 0, end: 0 }, + )), + }, + } +} diff --git a/crates/nu-cli/tests/test_completions.rs b/crates/nu-cli/tests/test_completions.rs index ec56a9779c..209edd6824 100644 --- a/crates/nu-cli/tests/test_completions.rs +++ b/crates/nu-cli/tests/test_completions.rs @@ -18,7 +18,7 @@ fn flag_completions() { let mut completer = NuCompleter::new(std::sync::Arc::new(engine), stack); // Test completions for the 'ls' flags - let suggestions = completer.complete("ls -".into(), 4); + let suggestions = completer.complete("ls -", 4); assert_eq!(12, suggestions.len()); diff --git a/crates/nu-command/src/filesystem/cd_query.rs b/crates/nu-command/src/filesystem/cd_query.rs index 6cdaa6537c..b6ad755b64 100644 --- a/crates/nu-command/src/filesystem/cd_query.rs +++ b/crates/nu-command/src/filesystem/cd_query.rs @@ -411,9 +411,9 @@ mod test { #[test] fn test_order_paths() { - fn sort<'a>(paths: &'a Vec<&'a str>, abbr: &str) -> Vec<&'a str> { + fn sort<'a>(paths: &'a [&'a str], abbr: &str) -> Vec<&'a str> { let abbr = Abbr::new_sanitized(abbr); - let mut paths = paths.clone(); + let mut paths = paths.to_owned(); paths.sort_by_key(|path| abbr.compare(path).unwrap()); paths diff --git a/crates/nu-glob/src/lib.rs b/crates/nu-glob/src/lib.rs index e2412d8969..ddaefc458d 100644 --- a/crates/nu-glob/src/lib.rs +++ b/crates/nu-glob/src/lib.rs @@ -966,7 +966,7 @@ mod test { .and_then(|p| match p.components().next().unwrap() { Component::Prefix(prefix_component) => { let path = Path::new(prefix_component.as_os_str()).join("*"); - Some(path.to_path_buf()) + Some(path) } _ => panic!("no prefix in this path"), }) diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 50c58ed0c3..3e0d72dcea 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -24,6 +24,28 @@ pub struct ParsedMenu { pub source: Value, } +/// Definition of a parsed menu from the config object +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Hooks { + pub pre_prompt: Option, + pub pre_execution: Option, +} + +impl Hooks { + pub fn new() -> Self { + Self { + pre_prompt: None, + pre_execution: None, + } + } +} + +impl Default for Hooks { + fn default() -> Self { + Self::new() + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Config { pub filesize_metric: bool, @@ -45,6 +67,7 @@ pub struct Config { pub log_level: String, pub keybindings: Vec, pub menus: Vec, + pub hooks: Hooks, pub rm_always_trash: bool, pub shell_integration: bool, pub buffer_editor: String, @@ -74,6 +97,7 @@ impl Default for Config { log_level: String::new(), keybindings: Vec::new(), menus: Vec::new(), + hooks: Hooks::new(), rm_always_trash: false, shell_integration: false, buffer_editor: String::new(), @@ -253,6 +277,13 @@ impl Value { eprintln!("{:?}", e); } }, + "hooks" => match create_hooks(value) { + Ok(hooks) => config.hooks = hooks, + Err(e) => { + eprintln!("$config.hooks is not a valid hooks list"); + eprintln!("{:?}", e); + } + }, "shell_integration" => { if let Ok(b) = value.as_bool() { config.shell_integration = b; @@ -343,6 +374,43 @@ pub fn color_value_string( } } +// Parse the hooks to find the blocks to run when the hooks fire +fn create_hooks(value: &Value) -> Result { + match value { + Value::Record { cols, vals, span } => { + let mut hooks = Hooks::new(); + + for idx in 0..cols.len() { + match cols[idx].as_str() { + "pre_prompt" => hooks.pre_prompt = Some(vals[idx].clone()), + "pre_execution" => hooks.pre_execution = Some(vals[idx].clone()), + x => { + return Err(ShellError::UnsupportedConfigValue( + "'pre_prompt' or 'pre_execution'".to_string(), + x.to_string(), + *span, + )); + } + } + } + + Ok(hooks) + } + v => match v.span() { + Ok(span) => Err(ShellError::UnsupportedConfigValue( + "record for 'hooks' config".into(), + "non-record value".into(), + span, + )), + _ => Err(ShellError::UnsupportedConfigValue( + "record for 'hooks' config".into(), + "non-record value".into(), + Span { start: 0, end: 0 }, + )), + }, + } +} + // Parses the config object to extract the strings that will compose a keybinding for reedline fn create_keybindings(value: &Value, config: &Config) -> Result, ShellError> { match value { diff --git a/docs/sample_config/default_config.nu b/docs/sample_config/default_config.nu index 6e680a5e8c..cd6945bb59 100644 --- a/docs/sample_config/default_config.nu +++ b/docs/sample_config/default_config.nu @@ -199,6 +199,14 @@ let-env config = { shell_integration: true # enables terminal markers and a workaround to arrow keys stop working issue disable_table_indexes: false # set to true to remove the index column from tables cd_with_abbreviations: false # set to true to allow you to do things like cd s/o/f and nushell expand it to cd some/other/folder + hooks: { + pre_prompt: { + $nothing # replace with source code to run before the prompt is shown + } + pre_execution: { + $nothing # replace with source code to run before the repl input is run + } + } menus: [ # Configuration for default nushell menus # Note the lack of souce parameter