diff --git a/src/edit.rs b/src/edit.rs index 578962fbe6..540f907095 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -277,6 +277,14 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { fn has_hint(&self) -> bool { self.hint.is_some() } + + fn line(&self) -> &str { + self.line.as_str() + } + + fn pos(&self) -> usize { + self.line.pos() + } } impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { diff --git a/src/keymap.rs b/src/keymap.rs index b43a5e98b8..ebbcb194f6 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -1,5 +1,6 @@ //! Bindings from keys to command for Emacs and Vi modes use std::collections::HashMap; +use std::fmt; use std::sync::{Arc, RwLock}; use log::debug; @@ -13,6 +14,59 @@ use crate::tty::{RawReader, Term, Terminal}; /// The number of times one command should be repeated. pub type RepeatCount = usize; +/// Custom dynamic action +#[derive(Clone)] +pub struct Action { + /// Currently used only for tracing + pub name: String, + /// Takes the currently edited `line` with the cursor `pos`ition and + /// returns the command to be performed or `None` to perform the default + /// one. + pub action: fn(line: &str, pos: usize, ctx: &ActionContext) -> Option, +} + +impl std::fmt::Debug for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Action").field("name", &self.name).finish() + } +} + +impl PartialEq for Action { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +pub struct ActionContext<'r> { + mode: EditMode, + input_mode: InputMode, + wrt: &'r dyn Refresher, +} + +impl<'r> ActionContext<'r> { + fn new(is: &InputState, wrt: &'r dyn Refresher) -> Self { + ActionContext { + mode: is.mode, + input_mode: is.input_mode, + wrt, + } + } + + pub fn mode(&self) -> EditMode { + self.mode + } + + /// vi only + pub fn input_mode(&self) -> InputMode { + self.input_mode + } + + /// Returns `true` if there is a hint displayed. + pub fn has_hint(&self) -> bool { + self.wrt.has_hint() + } +} + /// Commands #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] @@ -45,6 +99,8 @@ pub enum Cmd { EndOfHistory, /// forward-search-history ForwardSearchHistory, + /// Custom dynamic action + Custom(Action), /// history-search-backward HistorySearchBackward, /// history-search-forward @@ -329,8 +385,9 @@ impl Movement { } } -#[derive(PartialEq)] -enum InputMode { +/// Vi input modes +#[derive(Clone, Copy, PartialEq)] +pub enum InputMode { /// Vi Command/Alternate Command, /// Insert/Input mode @@ -376,6 +433,10 @@ pub trait Refresher { fn is_cursor_at_end(&self) -> bool; /// Returns `true` if there is a hint displayed. fn has_hint(&self) -> bool; + /// currently edited line + fn line(&self) -> &str; + /// Current cursor position (byte position) + fn pos(&self) -> usize; } impl InputState { @@ -460,6 +521,21 @@ impl InputState { } } + fn custom_binding(&self, wrt: &mut dyn Refresher, key: KeyEvent) -> Option { + let bindings = self.custom_bindings.read().unwrap(); + let cmd = bindings.get(&key); + if let Some(cmd) = cmd { + debug!(target: "rustyline", "Custom command: {:?}", cmd); + if let Cmd::Custom(action) = cmd { + let ctx = ActionContext::new(self, wrt); + return (action.action)(wrt.line(), wrt.pos(), &ctx); + } else { + return Some(cmd.clone()); + } + } + None + } + fn emacs( &mut self, rdr: &mut R, @@ -472,16 +548,13 @@ impl InputState { key = self.emacs_digit_argument(rdr, wrt, digit)?; } let (n, positive) = self.emacs_num_args(); // consume them in all cases - { - let bindings = self.custom_bindings.read().unwrap(); - if let Some(cmd) = bindings.get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - cmd.redo(Some(n), wrt) - } else { - cmd.clone() - }); - } + + if let Some(cmd) = self.custom_binding(wrt, key) { + return Ok(if cmd.is_repeatable() { + cmd.redo(Some(n), wrt) + } else { + cmd + }); } let cmd = match key { E(K::Char(c), M::NONE) => { @@ -622,20 +695,16 @@ impl InputState { } let no_num_args = self.num_args == 0; let n = self.vi_num_args(); // consume them in all cases - { - let bindings = self.custom_bindings.read().unwrap(); - if let Some(cmd) = bindings.get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - if no_num_args { - cmd.redo(None, wrt) - } else { - cmd.redo(Some(n), wrt) - } + if let Some(cmd) = self.custom_binding(wrt, key) { + return Ok(if cmd.is_repeatable() { + if no_num_args { + cmd.redo(None, wrt) } else { - cmd.clone() - }); - } + cmd.redo(Some(n), wrt) + } + } else { + cmd + }); } let cmd = match key { E(K::Char('$'), M::NONE) | E(K::End, M::NONE) => Cmd::Move(Movement::EndOfLine), @@ -800,16 +869,12 @@ impl InputState { wrt: &mut dyn Refresher, key: KeyEvent, ) -> Result { - { - let bindings = self.custom_bindings.read().unwrap(); - if let Some(cmd) = bindings.get(&key) { - debug!(target: "rustyline", "Custom command: {:?}", cmd); - return Ok(if cmd.is_repeatable() { - cmd.redo(None, wrt) - } else { - cmd.clone() - }); - } + if let Some(cmd) = self.custom_binding(wrt, key) { + return Ok(if cmd.is_repeatable() { + cmd.redo(None, wrt) + } else { + cmd + }); } let cmd = match key { E(K::Char(c), M::NONE) => { diff --git a/src/lib.rs b/src/lib.rs index 1a7f4aa486..82d6a4b062 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,9 @@ use crate::edit::State; use crate::highlight::Highlighter; use crate::hint::Hinter; use crate::history::{Direction, History}; -pub use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; +pub use crate::keymap::{ + Action, Anchor, At, CharSearch, Cmd, InputMode, Movement, RepeatCount, Word, +}; use crate::keymap::{InputState, Refresher}; pub use crate::keys::{KeyCode, KeyEvent, Modifiers}; use crate::kill_ring::KillRing;