From 9ee4086dfa7839e2e2b8147d43d1e53af4a715b9 Mon Sep 17 00:00:00 2001 From: unrelentingtech Date: Fri, 9 Sep 2022 23:31:32 +0300 Subject: [PATCH] Add a 'commandline' command for manipulating the current buffer (#6492) * Add a 'commandline' command for manipulating the current buffer from `executehostcommand` keybindings. Inspired by fish: https://fishshell.com/docs/current/cmds/commandline.html * Update to development reedline Includes nushell/reedline#472 Co-authored-by: sholderbach --- Cargo.lock | 3 +- Cargo.toml | 2 + crates/nu-cli/src/repl.rs | 28 ++++++- .../src/core_commands/commandline.rs | 84 +++++++++++++++++++ crates/nu-command/src/core_commands/mod.rs | 2 + crates/nu-command/src/default_context.rs | 1 + crates/nu-protocol/src/engine/engine_state.rs | 18 +++- 7 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 crates/nu-command/src/core_commands/commandline.rs diff --git a/Cargo.lock b/Cargo.lock index 1561cec7f..821c5493e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4079,8 +4079,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5559b5ab4817b0da0c6fc6814edfae537209e01d955a2f3e7595606e3d039691" +source = "git+https://github.com/nushell/reedline?branch=main#dc091e828590de6fd335af3f1d001ede851ac20a" dependencies = [ "chrono", "crossterm 0.24.0", diff --git a/Cargo.toml b/Cargo.toml index 5b358f82b..97c958e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,3 +122,5 @@ debug = false name = "nu" path = "src/main.rs" +[patch.crates-io] +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 1275cd427..e6e51b99e 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -14,11 +14,11 @@ use nu_engine::{convert_env_values, eval_block}; use nu_parser::{lex, parse}; use nu_protocol::{ ast::PathMember, - engine::{EngineState, Stack, StateWorkingSet}, + engine::{EngineState, ReplOperation, Stack, StateWorkingSet}, format_duration, BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, Spanned, Type, Value, VarId, }; -use reedline::{DefaultHinter, Emacs, SqliteBackedHistory, Vi}; +use reedline::{DefaultHinter, EditCommand, Emacs, SqliteBackedHistory, Vi}; use std::io::{self, Write}; use std::{sync::atomic::Ordering, time::Instant}; use strip_ansi_escapes::strip; @@ -347,6 +347,12 @@ pub fn evaluate_repl( .into_diagnostic()?; // todo: don't stop repl if error here? } + engine_state + .repl_buffer_state + .lock() + .expect("repl buffer state mutex") + .replace(line_editor.current_buffer_contents().to_string()); + // 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() { @@ -489,6 +495,24 @@ pub fn evaluate_repl( } run_ansi_sequence(RESET_APPLICATION_MODE)?; } + + let mut ops = engine_state + .repl_operation_queue + .lock() + .expect("repl op queue mutex"); + while let Some(op) = ops.pop_front() { + match op { + ReplOperation::Append(s) => line_editor.run_edit_commands(&[ + EditCommand::MoveToEnd, + EditCommand::InsertString(s), + ]), + ReplOperation::Insert(s) => { + line_editor.run_edit_commands(&[EditCommand::InsertString(s)]) + } + ReplOperation::Replace(s) => line_editor + .run_edit_commands(&[EditCommand::Clear, EditCommand::InsertString(s)]), + } + } } Ok(Signal::CtrlC) => { // `Reedline` clears the line content. New prompt is shown diff --git a/crates/nu-command/src/core_commands/commandline.rs b/crates/nu-command/src/core_commands/commandline.rs new file mode 100644 index 000000000..3561d2129 --- /dev/null +++ b/crates/nu-command/src/core_commands/commandline.rs @@ -0,0 +1,84 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::ReplOperation; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::IntoPipelineData; +use nu_protocol::{PipelineData, ShellError, Signature, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct Commandline; + +impl Command for Commandline { + fn name(&self) -> &str { + "commandline" + } + + fn signature(&self) -> Signature { + Signature::build("commandline") + .switch( + "append", + "appends the string to the end of the buffer", + Some('a'), + ) + .switch( + "insert", + "inserts the string into the buffer at the cursor position", + Some('i'), + ) + .switch( + "replace", + "replaces the current contents of the buffer (default)", + Some('r'), + ) + .optional( + "cmd", + SyntaxShape::String, + "the string to perform the operation with", + ) + .category(Category::Core) + } + + fn usage(&self) -> &str { + "View or modify the current command line input buffer" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["repl", "interactive"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + if let Some(cmd) = call.opt::(engine_state, stack, 0)? { + let mut ops = engine_state + .repl_operation_queue + .lock() + .expect("repl op queue mutex"); + ops.push_back(if call.has_flag("append") { + ReplOperation::Append(cmd.as_string()?) + } else if call.has_flag("insert") { + ReplOperation::Insert(cmd.as_string()?) + } else { + ReplOperation::Replace(cmd.as_string()?) + }); + Ok(Value::Nothing { span: call.head }.into_pipeline_data()) + } else if let Some(ref cmd) = *engine_state + .repl_buffer_state + .lock() + .expect("repl buffer state mutex") + { + Ok(Value::String { + val: cmd.clone(), + span: call.head, + } + .into_pipeline_data()) + } else { + Ok(Value::Nothing { span: call.head }.into_pipeline_data()) + } + } +} diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs index 1f590f49f..c1a40552a 100644 --- a/crates/nu-command/src/core_commands/mod.rs +++ b/crates/nu-command/src/core_commands/mod.rs @@ -1,5 +1,6 @@ mod alias; mod ast; +mod commandline; mod debug; mod def; mod def_env; @@ -30,6 +31,7 @@ mod version; pub use alias::Alias; pub use ast::Ast; +pub use commandline::Commandline; pub use debug::Debug; pub use def::Def; pub use def_env::DefEnv; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 369d7bd86..43e3d741a 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -30,6 +30,7 @@ pub fn create_default_context() -> EngineState { bind_command! { Alias, Ast, + Commandline, Debug, Def, DefEnv, diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index ff5e73cee..11f7d5cd9 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -6,16 +6,24 @@ use crate::{ }; use core::panic; use std::borrow::Borrow; -use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; use std::{ - collections::HashMap, - sync::{atomic::AtomicBool, Arc}, + collections::{HashMap, HashSet, VecDeque}, + sync::{atomic::AtomicBool, Arc, Mutex}, }; static PWD_ENV: &str = "PWD"; +// TODO: move to different file? where? +/// An operation to be performed with the current buffer of the interactive shell. +#[derive(Clone)] +pub enum ReplOperation { + Append(String), + Insert(String), + Replace(String), +} + /// The core global engine state. This includes all global definitions as well as any global state that /// will persist for the whole session. /// @@ -72,6 +80,8 @@ pub struct EngineState { pub env_vars: EnvVars, pub previous_env_vars: HashMap, pub config: Config, + pub repl_buffer_state: Arc>>, + pub repl_operation_queue: Arc>>, #[cfg(feature = "plugin")] pub plugin_signatures: Option, #[cfg(not(windows))] @@ -110,6 +120,8 @@ impl EngineState { env_vars: EnvVars::from([(DEFAULT_OVERLAY_NAME.to_string(), HashMap::new())]), previous_env_vars: HashMap::new(), config: Config::default(), + repl_buffer_state: Arc::new(Mutex::new(None)), + repl_operation_queue: Arc::new(Mutex::new(VecDeque::new())), #[cfg(feature = "plugin")] plugin_signatures: None, #[cfg(not(windows))]