From b35c4407e3c794c9a2632f7d2829544937d9ba21 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 12:16:40 -0500 Subject: [PATCH 1/8] implement multiple key vi insert mode keybindings --- README.md | 39 +++++ src/edit_mode/base.rs | 10 ++ src/edit_mode/emacs.rs | 153 ++++++++++++------ src/edit_mode/keybindings.rs | 306 +++++++++++++++++++++++++++++++++++ src/edit_mode/mod.rs | 2 +- src/edit_mode/vi/mod.rs | 208 +++++++++++++++++------- src/engine.rs | 85 ++++++++-- src/lib.rs | 41 ++++- 8 files changed, 723 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index c2dc7455..2ff8268f 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,45 @@ let edit_mode = Box::new(Emacs::new(keybindings)); let mut line_editor = Reedline::create().with_edit_mode(edit_mode); ``` +### Integrate with key sequences + +```rust +// Configure reedline with key sequence bindings (like "jj" in vi insert mode) + +use { + crossterm::event::{KeyCode, KeyModifiers}, + reedline::{ + default_vi_insert_keybindings, default_vi_normal_keybindings, KeyCombination, Reedline, + ReedlineEvent, Vi, + }, +}; + +let mut insert_keybindings = default_vi_insert_keybindings(); +insert_keybindings.add_sequence_binding( + vec![ + KeyCombination { + modifier: KeyModifiers::NONE, + key_code: KeyCode::Char('j'), + }, + KeyCombination { + modifier: KeyModifiers::NONE, + key_code: KeyCode::Char('j'), + }, + ], + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Esc, + ReedlineEvent::ViChangeMode("normal".into()), + ReedlineEvent::Repaint, + ]), +); + +let edit_mode = Box::new(Vi::new( + insert_keybindings, + default_vi_normal_keybindings(), +)); +let mut line_editor = Reedline::create().with_edit_mode(edit_mode); +``` + ### Integrate with `History` ```rust,no_run diff --git a/src/edit_mode/base.rs b/src/edit_mode/base.rs index 408098b3..dd04c0de 100644 --- a/src/edit_mode/base.rs +++ b/src/edit_mode/base.rs @@ -18,4 +18,14 @@ pub trait EditMode: Send { fn handle_mode_specific_event(&mut self, _event: ReedlineEvent) -> EventStatus { EventStatus::Inapplicable } + + /// Whether a key sequence is currently pending + fn has_pending_sequence(&self) -> bool { + false + } + + /// Flush any pending key sequence and return the resulting event + fn flush_pending_sequence(&mut self) -> Option { + None + } } diff --git a/src/edit_mode/emacs.rs b/src/edit_mode/emacs.rs index 155a0741..b7c88c37 100644 --- a/src/edit_mode/emacs.rs +++ b/src/edit_mode/emacs.rs @@ -2,7 +2,8 @@ use crate::{ edit_mode::{ keybindings::{ add_common_control_bindings, add_common_edit_bindings, add_common_navigation_bindings, - add_common_selection_bindings, edit_bind, Keybindings, + add_common_selection_bindings, edit_bind, KeyCombination, KeySequenceState, + Keybindings, }, EditMode, }, @@ -105,12 +106,14 @@ pub fn default_emacs_keybindings() -> Keybindings { /// This parses the incoming Events like a emacs style-editor pub struct Emacs { keybindings: Keybindings, + sequence_state: KeySequenceState, } impl Default for Emacs { fn default() -> Self { Emacs { keybindings: default_emacs_keybindings(), + sequence_state: KeySequenceState::default(), } } } @@ -120,69 +123,113 @@ impl EditMode for Emacs { match event.into() { Event::Key(KeyEvent { code, modifiers, .. - }) => match (modifiers, code) { - (modifier, KeyCode::Char(c)) => { - // Note. The modifier can also be a combination of modifiers, for - // example: - // KeyModifiers::CONTROL | KeyModifiers::ALT - // KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT - // - // Mixed modifiers are used by non american keyboards that have extra - // keys like 'alt gr'. Keep this in mind if in the future there are - // cases where an event is not being captured - let c = match modifier { - KeyModifiers::NONE => c, - _ => c.to_ascii_lowercase(), - }; - - self.keybindings - .find_binding(modifier, KeyCode::Char(c)) - .unwrap_or_else(|| { - if modifier == KeyModifiers::NONE - || modifier == KeyModifiers::SHIFT - || modifier == KeyModifiers::CONTROL | KeyModifiers::ALT - || modifier - == KeyModifiers::CONTROL - | KeyModifiers::ALT - | KeyModifiers::SHIFT - { - ReedlineEvent::Edit(vec![EditCommand::InsertChar( - if modifier == KeyModifiers::SHIFT { - c.to_ascii_uppercase() - } else { - c - }, - )]) - } else { - ReedlineEvent::None - } - }) - } - _ => self - .keybindings - .find_binding(modifiers, code) - .unwrap_or(ReedlineEvent::None), - }, - - Event::Mouse(_) => ReedlineEvent::Mouse, - Event::Resize(width, height) => ReedlineEvent::Resize(width, height), - Event::FocusGained => ReedlineEvent::None, - Event::FocusLost => ReedlineEvent::None, - Event::Paste(body) => ReedlineEvent::Edit(vec![EditCommand::InsertString( - body.replace("\r\n", "\n").replace('\r', "\n"), - )]), + }) => { + let combo = Self::normalize_key_combo(modifiers, code); + let keybindings = &self.keybindings; + self.sequence_state + .process_combo(keybindings, combo, |combo| { + Self::single_key_event(keybindings, combo) + }) + .unwrap_or(ReedlineEvent::None) + } + + Event::Mouse(_) => self.with_flushed_sequence(ReedlineEvent::Mouse), + Event::Resize(width, height) => { + self.with_flushed_sequence(ReedlineEvent::Resize(width, height)) + } + Event::FocusGained => self.with_flushed_sequence(ReedlineEvent::None), + Event::FocusLost => self.with_flushed_sequence(ReedlineEvent::None), + Event::Paste(body) => { + self.with_flushed_sequence(ReedlineEvent::Edit(vec![EditCommand::InsertString( + body.replace("\r\n", "\n").replace('\r', "\n"), + )])) + } } } fn edit_mode(&self) -> PromptEditMode { PromptEditMode::Emacs } + + fn has_pending_sequence(&self) -> bool { + self.sequence_state.is_pending() + } + + fn flush_pending_sequence(&mut self) -> Option { + let keybindings = &self.keybindings; + self.sequence_state + .flush(|combo| Self::single_key_event(keybindings, combo)) + } } impl Emacs { /// Emacs style input parsing constructor if you want to use custom keybindings pub const fn new(keybindings: Keybindings) -> Self { - Emacs { keybindings } + Emacs { + keybindings, + sequence_state: KeySequenceState::new(), + } + } + + fn normalize_key_combo(modifier: KeyModifiers, code: KeyCode) -> KeyCombination { + let key_code = match code { + KeyCode::Char(c) => { + let c = match modifier { + KeyModifiers::NONE => c, + _ => c.to_ascii_lowercase(), + }; + KeyCode::Char(c) + } + other => other, + }; + + KeyCombination { modifier, key_code } + } + + fn single_key_event(keybindings: &Keybindings, combo: KeyCombination) -> ReedlineEvent { + match combo.key_code { + KeyCode::Char(c) => keybindings + .find_binding(combo.modifier, KeyCode::Char(c)) + .unwrap_or_else(|| { + if combo.modifier == KeyModifiers::NONE + || combo.modifier == KeyModifiers::SHIFT + || combo.modifier == KeyModifiers::CONTROL | KeyModifiers::ALT + || combo.modifier + == KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT + { + ReedlineEvent::Edit(vec![EditCommand::InsertChar( + if combo.modifier == KeyModifiers::SHIFT { + c.to_ascii_uppercase() + } else { + c + }, + )]) + } else { + ReedlineEvent::None + } + }), + code => keybindings + .find_binding(combo.modifier, code) + .unwrap_or(ReedlineEvent::None), + } + } + + fn with_flushed_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { + let Some(flush_event) = self.flush_pending_sequence() else { + return event; + }; + + if matches!(event, ReedlineEvent::None) { + return flush_event; + } + + match flush_event { + ReedlineEvent::Multiple(mut events) => { + events.push(event); + ReedlineEvent::Multiple(events) + } + other => ReedlineEvent::Multiple(vec![other, event]), + } } } diff --git a/src/edit_mode/keybindings.rs b/src/edit_mode/keybindings.rs index ef4636c7..9c71b77e 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -11,11 +11,31 @@ pub struct KeyCombination { pub key_code: KeyCode, } +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] +pub struct KeySequence(pub Vec); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum KeySequenceMatch { + Exact(ReedlineEvent), + Prefix, + ExactAndPrefix(ReedlineEvent), + NoMatch, +} + +#[derive(Debug, Default, Clone)] +pub struct KeySequenceState { + buffer: Vec, + pending_exact: Option<(usize, ReedlineEvent)>, +} + /// Main definition of editor keybindings #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Keybindings { /// Defines a keybinding for a reedline event pub bindings: HashMap, + /// Defines a key sequence binding for a reedline event + #[serde(default)] + pub sequence_bindings: HashMap, } impl Default for Keybindings { @@ -29,6 +49,7 @@ impl Keybindings { pub fn new() -> Self { Self { bindings: HashMap::new(), + sequence_bindings: HashMap::new(), } } @@ -59,12 +80,57 @@ impl Keybindings { self.bindings.insert(key_combo, command); } + /// Adds a key sequence binding + /// + /// # Panics + /// + /// If `sequence` is empty or `command` is an empty [`ReedlineEvent::UntilFound`] + pub fn add_sequence_binding(&mut self, sequence: Vec, command: ReedlineEvent) { + assert!( + !sequence.is_empty(), + "Key sequence must contain at least one key" + ); + + if let ReedlineEvent::UntilFound(subcommands) = &command { + assert!( + !subcommands.is_empty(), + "UntilFound should contain a series of potential events to handle" + ); + } + + self.sequence_bindings + .insert(KeySequence(sequence), command); + } + /// Find a keybinding based on the modifier and keycode pub fn find_binding(&self, modifier: KeyModifiers, key_code: KeyCode) -> Option { let key_combo = KeyCombination { modifier, key_code }; self.bindings.get(&key_combo).cloned() } + /// Find how a key sequence matches existing bindings + pub fn sequence_match(&self, sequence: &[KeyCombination]) -> KeySequenceMatch { + if sequence.is_empty() || self.sequence_bindings.is_empty() { + return KeySequenceMatch::NoMatch; + } + + let exact = self + .sequence_bindings + .get(&KeySequence(sequence.to_vec())) + .cloned(); + + let is_prefix = self.sequence_bindings.keys().any(|key_sequence| { + key_sequence.0.len() > sequence.len() && key_sequence.0[..sequence.len()] == *sequence + }); + + match (exact, is_prefix) { + (Some(event), true) => KeySequenceMatch::ExactAndPrefix(event), + (Some(event), false) => KeySequenceMatch::Exact(event), + (None, true) => KeySequenceMatch::Prefix, + (None, false) => KeySequenceMatch::NoMatch, + } + } + /// Remove a keybinding /// /// Returns `Some(ReedlineEvent)` if the key combination was previously bound to a particular [`ReedlineEvent`] @@ -77,10 +143,169 @@ impl Keybindings { self.bindings.remove(&key_combo) } + /// Remove a key sequence binding + /// + /// Returns `Some(ReedlineEvent)` if the key sequence was previously bound to a particular [`ReedlineEvent`] + pub fn remove_sequence_binding( + &mut self, + sequence: Vec, + ) -> Option { + self.sequence_bindings.remove(&KeySequence(sequence)) + } + /// Get assigned keybindings pub fn get_keybindings(&self) -> &HashMap { &self.bindings } + + /// Get assigned sequence bindings + pub fn get_sequence_keybindings(&self) -> &HashMap { + &self.sequence_bindings + } +} + +impl KeySequenceState { + pub const fn new() -> Self { + Self { + buffer: Vec::new(), + pending_exact: None, + } + } + + pub fn is_pending(&self) -> bool { + !self.buffer.is_empty() + } + + pub fn clear(&mut self) { + self.buffer.clear(); + self.pending_exact = None; + } + + pub fn process_combo( + &mut self, + keybindings: &Keybindings, + combo: KeyCombination, + mut fallback: F, + ) -> Option + where + F: FnMut(KeyCombination) -> ReedlineEvent, + { + if keybindings.sequence_bindings.is_empty() { + self.clear(); + return Some(fallback(combo)); + } + + self.buffer.push(combo); + + let mut events = Vec::new(); + + loop { + match keybindings.sequence_match(&self.buffer) { + KeySequenceMatch::Exact(event) => { + self.buffer.clear(); + self.pending_exact = None; + events.push(event); + break; + } + KeySequenceMatch::ExactAndPrefix(event) => { + self.pending_exact = Some((self.buffer.len(), event)); + break; + } + KeySequenceMatch::Prefix => { + self.pending_exact = None; + break; + } + KeySequenceMatch::NoMatch => { + if let Some((pending_len, pending_event)) = self.pending_exact.take() { + let remaining = if pending_len < self.buffer.len() { + self.buffer.split_off(pending_len) + } else { + Vec::new() + }; + self.buffer.clear(); + self.buffer = remaining; + events.push(pending_event); + if self.buffer.is_empty() { + break; + } + continue; + } + + let flushed = self.buffer.remove(0); + let event = fallback(flushed); + if !matches!(event, ReedlineEvent::None) { + events.push(event); + } + + if self.buffer.is_empty() { + break; + } + } + } + } + + let mut events: Vec = events + .into_iter() + .filter(|event| !matches!(event, ReedlineEvent::None)) + .collect(); + + if events.is_empty() { + return None; + } + + if events.len() == 1 { + return Some(events.remove(0)); + } + + Some(ReedlineEvent::Multiple(events)) + } + + pub fn flush(&mut self, mut fallback: F) -> Option + where + F: FnMut(KeyCombination) -> ReedlineEvent, + { + if self.buffer.is_empty() { + self.pending_exact = None; + return None; + } + + if let Some((pending_len, pending_event)) = self.pending_exact.take() { + let remaining = if pending_len < self.buffer.len() { + self.buffer.split_off(pending_len) + } else { + Vec::new() + }; + self.buffer.clear(); + + let mut events = vec![pending_event]; + for combo in remaining { + let event = fallback(combo); + if !matches!(event, ReedlineEvent::None) { + events.push(event); + } + } + + return match events.len() { + 0 => None, + 1 => Some(events.remove(0)), + _ => Some(ReedlineEvent::Multiple(events)), + }; + } + + let mut events = Vec::new(); + for combo in self.buffer.drain(..) { + let event = fallback(combo); + if !matches!(event, ReedlineEvent::None) { + events.push(event); + } + } + + match events.len() { + 0 => None, + 1 => Some(events.remove(0)), + _ => Some(ReedlineEvent::Multiple(events)), + } + } } pub fn edit_bind(command: EditCommand) -> ReedlineEvent { @@ -288,3 +513,84 @@ pub fn add_common_selection_bindings(kb: &mut Keybindings) { edit_bind(EC::SelectAll), ); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::EditCommand; + + fn combo(c: char) -> KeyCombination { + KeyCombination { + modifier: KeyModifiers::NONE, + key_code: KeyCode::Char(c), + } + } + + fn fallback(combo: KeyCombination) -> ReedlineEvent { + match combo.key_code { + KeyCode::Char(c) => ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)]), + _ => ReedlineEvent::None, + } + } + + #[test] + fn sequence_match_detects_prefix_and_exact() { + let mut keybindings = Keybindings::new(); + keybindings.add_sequence_binding(vec![combo('j'), combo('j')], ReedlineEvent::Esc); + + assert!(matches!( + keybindings.sequence_match(&[combo('j')]), + KeySequenceMatch::Prefix + )); + assert!(matches!( + keybindings.sequence_match(&[combo('j'), combo('j')]), + KeySequenceMatch::Exact(ReedlineEvent::Esc) + )); + } + + #[test] + fn sequence_state_emits_on_match() { + let mut keybindings = Keybindings::new(); + keybindings.add_sequence_binding(vec![combo('j'), combo('j')], ReedlineEvent::Esc); + + let mut state = KeySequenceState::default(); + let first = state.process_combo(&keybindings, combo('j'), fallback); + assert_eq!(first, None); + + let second = state.process_combo(&keybindings, combo('j'), fallback); + assert_eq!(second, Some(ReedlineEvent::Esc)); + } + + #[test] + fn sequence_state_flushes_on_miss() { + let mut keybindings = Keybindings::new(); + keybindings.add_sequence_binding(vec![combo('j'), combo('j')], ReedlineEvent::Esc); + + let mut state = KeySequenceState::default(); + let _ = state.process_combo(&keybindings, combo('j'), fallback); + let second = state.process_combo(&keybindings, combo('k'), fallback); + + assert_eq!( + second, + Some(ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::InsertChar('j')]), + ReedlineEvent::Edit(vec![EditCommand::InsertChar('k')]), + ])) + ); + } + + #[test] + fn sequence_state_flushes_pending_on_timeout() { + let mut keybindings = Keybindings::new(); + keybindings.add_sequence_binding(vec![combo('j'), combo('j')], ReedlineEvent::Esc); + + let mut state = KeySequenceState::default(); + let _ = state.process_combo(&keybindings, combo('j'), fallback); + let flushed = state.flush(fallback); + + assert_eq!( + flushed, + Some(ReedlineEvent::Edit(vec![EditCommand::InsertChar('j')])) + ); + } +} diff --git a/src/edit_mode/mod.rs b/src/edit_mode/mod.rs index 38e1456f..3e371b46 100644 --- a/src/edit_mode/mod.rs +++ b/src/edit_mode/mod.rs @@ -7,5 +7,5 @@ mod vi; pub use base::EditMode; pub use cursors::CursorConfig; pub use emacs::{default_emacs_keybindings, Emacs}; -pub use keybindings::Keybindings; +pub use keybindings::{KeyCombination, KeySequence, KeySequenceState, Keybindings}; pub use vi::{default_vi_insert_keybindings, default_vi_normal_keybindings, Vi}; diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index a3204adb..f83590f7 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -12,7 +12,7 @@ use self::motion::ViCharSearch; use super::EditMode; use crate::{ - edit_mode::{keybindings::Keybindings, vi::parser::parse}, + edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, enums::{EditCommand, EventStatus, ReedlineEvent, ReedlineRawEvent}, PromptEditMode, PromptViMode, }; @@ -42,6 +42,7 @@ pub struct Vi { cache: Vec, insert_keybindings: Keybindings, normal_keybindings: Keybindings, + insert_sequence_state: KeySequenceState, mode: ViMode, previous: Option, // last f, F, t, T motion for ; and , @@ -54,6 +55,7 @@ impl Default for Vi { insert_keybindings: default_vi_insert_keybindings(), normal_keybindings: default_vi_normal_keybindings(), cache: Vec::new(), + insert_sequence_state: KeySequenceState::default(), mode: ViMode::Insert, previous: None, last_char_search: None, @@ -118,45 +120,12 @@ impl EditMode for Vi { } } (ViMode::Insert, modifier, KeyCode::Char(c)) => { - // Note. The modifier can also be a combination of modifiers, for - // example: - // KeyModifiers::CONTROL | KeyModifiers::ALT - // KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT - // - // Mixed modifiers are used by non american keyboards that have extra - // keys like 'alt gr'. Keep this in mind if in the future there are - // cases where an event is not being captured - let c = match modifier { - KeyModifiers::NONE => c, - _ => c.to_ascii_lowercase(), - }; - - self.insert_keybindings - .find_binding(modifier, KeyCode::Char(c)) - .unwrap_or_else(|| { - if modifier == KeyModifiers::NONE - || modifier == KeyModifiers::SHIFT - || modifier == KeyModifiers::CONTROL | KeyModifiers::ALT - || modifier - == KeyModifiers::CONTROL - | KeyModifiers::ALT - | KeyModifiers::SHIFT - { - ReedlineEvent::Edit(vec![EditCommand::InsertChar( - if modifier == KeyModifiers::SHIFT { - c.to_ascii_uppercase() - } else { - c - }, - )]) - } else { - ReedlineEvent::None - } - }) + self.handle_insert_key(modifier, KeyCode::Char(c)) } (_, KeyModifiers::NONE, KeyCode::Esc) => { self.cache.clear(); self.mode = ViMode::Normal; + self.insert_sequence_state.clear(); ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) } (ViMode::Normal | ViMode::Visual, _, _) => self @@ -171,26 +140,18 @@ impl EditMode for Vi { ReedlineEvent::None } }), - (ViMode::Insert, _, _) => self - .insert_keybindings - .find_binding(modifiers, code) - .unwrap_or_else(|| { - // Default Enter behavior when no custom binding - if modifiers == KeyModifiers::NONE && code == KeyCode::Enter { - ReedlineEvent::Enter - } else { - ReedlineEvent::None - } - }), + (ViMode::Insert, _, _) => self.handle_insert_key(modifiers, code), }, - Event::Mouse(_) => ReedlineEvent::Mouse, - Event::Resize(width, height) => ReedlineEvent::Resize(width, height), - Event::FocusGained => ReedlineEvent::None, - Event::FocusLost => ReedlineEvent::None, - Event::Paste(body) => ReedlineEvent::Edit(vec![EditCommand::InsertString( - body.replace("\r\n", "\n").replace('\r', "\n"), - )]), + Event::Mouse(_) => self.with_flushed_insert_sequence(ReedlineEvent::Mouse), + Event::Resize(width, height) => { + self.with_flushed_insert_sequence(ReedlineEvent::Resize(width, height)) + } + Event::FocusGained => self.with_flushed_insert_sequence(ReedlineEvent::None), + Event::FocusLost => self.with_flushed_insert_sequence(ReedlineEvent::None), + Event::Paste(body) => self.with_flushed_insert_sequence(ReedlineEvent::Edit(vec![ + EditCommand::InsertString(body.replace("\r\n", "\n").replace('\r', "\n")), + ])), } } @@ -206,6 +167,7 @@ impl EditMode for Vi { ReedlineEvent::ViChangeMode(mode_str) => match ViMode::from_str(&mode_str) { Ok(mode) => { self.mode = mode; + self.insert_sequence_state.clear(); EventStatus::Handled } Err(_) => EventStatus::Inapplicable, @@ -213,11 +175,105 @@ impl EditMode for Vi { _ => EventStatus::Inapplicable, } } + + fn has_pending_sequence(&self) -> bool { + matches!(self.mode, ViMode::Insert) && self.insert_sequence_state.is_pending() + } + + fn flush_pending_sequence(&mut self) -> Option { + if !matches!(self.mode, ViMode::Insert) { + return None; + } + + let keybindings = &self.insert_keybindings; + self.insert_sequence_state + .flush(|combo| Self::insert_single_key_event(keybindings, combo)) + } +} + +impl Vi { + fn normalize_key_combo(modifier: KeyModifiers, code: KeyCode) -> KeyCombination { + let key_code = match code { + KeyCode::Char(c) => { + let c = match modifier { + KeyModifiers::NONE => c, + _ => c.to_ascii_lowercase(), + }; + KeyCode::Char(c) + } + other => other, + }; + + KeyCombination { modifier, key_code } + } + + fn insert_single_key_event(keybindings: &Keybindings, combo: KeyCombination) -> ReedlineEvent { + match combo.key_code { + KeyCode::Char(c) => keybindings + .find_binding(combo.modifier, KeyCode::Char(c)) + .unwrap_or_else(|| { + if combo.modifier == KeyModifiers::NONE + || combo.modifier == KeyModifiers::SHIFT + || combo.modifier == KeyModifiers::CONTROL | KeyModifiers::ALT + || combo.modifier + == KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT + { + ReedlineEvent::Edit(vec![EditCommand::InsertChar( + if combo.modifier == KeyModifiers::SHIFT { + c.to_ascii_uppercase() + } else { + c + }, + )]) + } else { + ReedlineEvent::None + } + }), + code => keybindings + .find_binding(combo.modifier, code) + .unwrap_or_else(|| { + if combo.modifier == KeyModifiers::NONE && code == KeyCode::Enter { + ReedlineEvent::Enter + } else { + ReedlineEvent::None + } + }), + } + } + + fn handle_insert_key(&mut self, modifier: KeyModifiers, code: KeyCode) -> ReedlineEvent { + let combo = Self::normalize_key_combo(modifier, code); + let keybindings = &self.insert_keybindings; + self.insert_sequence_state + .process_combo(keybindings, combo, |combo| { + Self::insert_single_key_event(keybindings, combo) + }) + .unwrap_or(ReedlineEvent::None) + } + + fn with_flushed_insert_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { + let Some(flush_event) = self.flush_pending_sequence() else { + return event; + }; + + if matches!(event, ReedlineEvent::None) { + return flush_event; + } + + match flush_event { + ReedlineEvent::Multiple(mut events) => { + events.push(event); + ReedlineEvent::Multiple(events) + } + other => ReedlineEvent::Multiple(vec![other, event]), + } + } } #[cfg(test)] mod test { use super::*; + use crate::KeyCombination; use pretty_assertions::assert_eq; #[test] @@ -332,4 +388,48 @@ mod test { assert_eq!(result, ReedlineEvent::None); } + + #[test] + fn insert_sequence_binding_emits_event() { + let mut insert_keybindings = default_vi_insert_keybindings(); + let exit_event = ReedlineEvent::Multiple(vec![ + ReedlineEvent::Esc, + ReedlineEvent::ViChangeMode("normal".to_string()), + ReedlineEvent::Repaint, + ]); + insert_keybindings.add_sequence_binding( + vec![ + KeyCombination { + modifier: KeyModifiers::NONE, + key_code: KeyCode::Char('j'), + }, + KeyCombination { + modifier: KeyModifiers::NONE, + key_code: KeyCode::Char('j'), + }, + ], + exit_event.clone(), + ); + + let mut vi = Vi { + insert_keybindings, + normal_keybindings: default_vi_normal_keybindings(), + mode: ViMode::Insert, + ..Default::default() + }; + + let first = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char('j'), + KeyModifiers::NONE, + ))) + .unwrap(); + let second = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char('j'), + KeyModifiers::NONE, + ))) + .unwrap(); + + assert_eq!(vi.parse_event(first), ReedlineEvent::None); + assert_eq!(vi.parse_event(second), exit_event); + } } diff --git a/src/engine.rs b/src/engine.rs index bdb04373..f1be8025 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -42,7 +42,8 @@ use { terminal, QueueableCommand, }, std::{ - fs::File, io, io::Result, io::Write, process::Command, time::Duration, time::SystemTime, + fs::File, io, io::Result, io::Write, process::Command, time::Duration, time::Instant, + time::SystemTime, }, }; @@ -168,6 +169,10 @@ pub struct Reedline { // Whether lines should be accepted immediately immediately_accept: bool, + // Pending key sequence state + key_sequence_timeout: Duration, + pending_sequence_started: Option, + #[cfg(feature = "external_printer")] external_printer: Option>, } @@ -242,6 +247,8 @@ impl Reedline { bracketed_paste: BracketedPasteGuard::default(), kitty_protocol: KittyProtocolGuard::default(), immediately_accept: false, + key_sequence_timeout: Duration::from_millis(300), + pending_sequence_started: None, #[cfg(feature = "external_printer")] external_printer: None, } @@ -555,6 +562,12 @@ impl Reedline { self } + /// A builder that configures the timeout for key sequence matching + pub fn with_key_sequence_timeout(mut self, timeout: Duration) -> Self { + self.key_sequence_timeout = timeout; + self + } + /// Returns the corresponding expected prompt style for the given edit mode pub fn prompt_edit_mode(&self) -> PromptEditMode { self.edit_mode.edit_mode() @@ -743,28 +756,59 @@ impl Reedline { } let mut events: Vec = vec![]; + let mut sequence_timed_out = false; if !self.immediately_accept { - // If the `external_printer` feature is enabled, we need to - // periodically yield so that external printers get a chance to - // print. Otherwise, we can just block until we receive an event. - #[cfg(feature = "external_printer")] - if event::poll(EXTERNAL_PRINTER_WAIT)? { + if self.edit_mode.has_pending_sequence() { + if self.pending_sequence_started.is_none() { + self.pending_sequence_started = Some(Instant::now()); + } + + let start = self + .pending_sequence_started + .expect("pending sequence start should be set"); + let elapsed = start.elapsed(); + + if elapsed >= self.key_sequence_timeout { + sequence_timed_out = true; + } else { + let remaining = self.key_sequence_timeout - elapsed; + #[cfg(feature = "external_printer")] + let poll_duration = remaining.min(EXTERNAL_PRINTER_WAIT).min(POLL_WAIT); + #[cfg(not(feature = "external_printer"))] + let poll_duration = remaining.min(POLL_WAIT); + + if event::poll(poll_duration)? { + events.push(crossterm::event::read()?); + } else if start.elapsed() >= self.key_sequence_timeout { + sequence_timed_out = true; + } + } + } else { + // If the `external_printer` feature is enabled, we need to + // periodically yield so that external printers get a chance to + // print. Otherwise, we can just block until we receive an event. + #[cfg(feature = "external_printer")] + if event::poll(EXTERNAL_PRINTER_WAIT)? { + events.push(crossterm::event::read()?); + } + #[cfg(not(feature = "external_printer"))] events.push(crossterm::event::read()?); } - #[cfg(not(feature = "external_printer"))] - events.push(crossterm::event::read()?); // Receive all events in the queue without blocking. Will stop when // a line of input is completed. - while !completed(&events) && event::poll(Duration::from_millis(0))? { - events.push(crossterm::event::read()?); + if !sequence_timed_out { + while !completed(&events) && event::poll(Duration::from_millis(0))? { + events.push(crossterm::event::read()?); + } } // If we believe there's text pasting or resizing going on, batch // more events at the cost of a slight delay. - if events.len() > EVENTS_THRESHOLD - || events.iter().any(|e| matches!(e, Event::Resize(_, _))) + if !sequence_timed_out + && (events.len() > EVENTS_THRESHOLD + || events.iter().any(|e| matches!(e, Event::Resize(_, _)))) { while !completed(&events) && event::poll(POLL_WAIT)? { events.push(crossterm::event::read()?); @@ -778,7 +822,9 @@ impl Reedline { let mut reedline_events: Vec = vec![]; let mut edits = vec![]; let mut resize = None; + let mut sequence_activity = false; for event in events { + let is_key_event = matches!(event, Event::Key(_)); if let Ok(event) = ReedlineRawEvent::try_from(event) { match self.edit_mode.parse_event(event) { ReedlineEvent::Edit(edit) => edits.extend(edit), @@ -791,6 +837,9 @@ impl Reedline { reedline_events.push(event); } } + if is_key_event && self.edit_mode.has_pending_sequence() { + sequence_activity = true; + } } } if !edits.is_empty() { @@ -799,6 +848,18 @@ impl Reedline { if let Some((x, y)) = resize { reedline_events.push(ReedlineEvent::Resize(x, y)); } + if sequence_timed_out { + if let Some(event) = self.edit_mode.flush_pending_sequence() { + reedline_events.push(event); + } + self.pending_sequence_started = None; + } else if self.edit_mode.has_pending_sequence() { + if sequence_activity { + self.pending_sequence_started = Some(Instant::now()); + } + } else { + self.pending_sequence_started = None; + } if self.immediately_accept { reedline_events.push(ReedlineEvent::Submit); } diff --git a/src/lib.rs b/src/lib.rs index d202a220..5b5441e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,45 @@ //! let mut line_editor = Reedline::create().with_edit_mode(edit_mode); //! ``` //! +//! ## Integrate with key sequences +//! +//! ```rust +//! // Configure reedline with key sequence bindings (like "jj" in vi insert mode) +//! +//! use { +//! crossterm::event::{KeyCode, KeyModifiers}, +//! reedline::{ +//! default_vi_insert_keybindings, default_vi_normal_keybindings, KeyCombination, Reedline, +//! ReedlineEvent, Vi, +//! }, +//! }; +//! +//! let mut insert_keybindings = default_vi_insert_keybindings(); +//! insert_keybindings.add_sequence_binding( +//! vec![ +//! KeyCombination { +//! modifier: KeyModifiers::NONE, +//! key_code: KeyCode::Char('j'), +//! }, +//! KeyCombination { +//! modifier: KeyModifiers::NONE, +//! key_code: KeyCode::Char('j'), +//! }, +//! ], +//! ReedlineEvent::Multiple(vec![ +//! ReedlineEvent::Esc, +//! ReedlineEvent::ViChangeMode("normal".into()), +//! ReedlineEvent::Repaint, +//! ]), +//! ); +//! +//! let edit_mode = Box::new(Vi::new( +//! insert_keybindings, +//! default_vi_normal_keybindings(), +//! )); +//! let mut line_editor = Reedline::create().with_edit_mode(edit_mode); +//! ``` +//! //! ## Integrate with [`History`] //! //! ```rust,no_run @@ -262,7 +301,7 @@ pub use prompt::{ mod edit_mode; pub use edit_mode::{ default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, - CursorConfig, EditMode, Emacs, Keybindings, Vi, + CursorConfig, EditMode, Emacs, KeyCombination, KeySequence, Keybindings, Vi, }; mod highlighter; From a406370fcd62cb9dbdd5e93f52e401181572e046 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 13:17:49 -0500 Subject: [PATCH 2/8] make key combinations more generic --- README.md | 6 +- src/edit_mode/keybindings.rs | 107 ++++++++++ src/edit_mode/vi/mod.rs | 319 ++++++++++++++++++----------- src/edit_mode/vi/vi_keybindings.rs | 15 +- src/engine.rs | 24 ++- src/enums.rs | 4 + src/lib.rs | 6 +- 7 files changed, 351 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 2ff8268f..f0fe7879 100644 --- a/README.md +++ b/README.md @@ -109,11 +109,7 @@ insert_keybindings.add_sequence_binding( key_code: KeyCode::Char('j'), }, ], - ReedlineEvent::Multiple(vec![ - ReedlineEvent::Esc, - ReedlineEvent::ViChangeMode("normal".into()), - ReedlineEvent::Repaint, - ]), + ReedlineEvent::ViExitToNormalMode, ); let edit_mode = Box::new(Vi::new( diff --git a/src/edit_mode/keybindings.rs b/src/edit_mode/keybindings.rs index 9c71b77e..45eaf903 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -5,12 +5,16 @@ use { std::collections::HashMap, }; +/// Key combination consisting of modifier(s) and a key code. #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] pub struct KeyCombination { + /// Key modifiers (e.g., Ctrl, Alt, Shift) pub modifier: KeyModifiers, + /// Key code (e.g., Char('a'), Enter) pub key_code: KeyCode, } +/// Sequence of key combinations. #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] pub struct KeySequence(pub Vec); @@ -22,12 +26,24 @@ pub enum KeySequenceMatch { NoMatch, } +/// State used to track partial key sequence matches. #[derive(Debug, Default, Clone)] pub struct KeySequenceState { buffer: Vec, pending_exact: Option<(usize, ReedlineEvent)>, } +/// Resolution result for processing a key sequence. +#[derive(Debug, Default, Clone)] +pub struct SequenceResolution { + /// Events emitted from matched sequences. + pub events: Vec, + /// Key combinations to flush through fallback handling. + pub combos: Vec, + /// Whether a sequence is pending further input. + pub pending: bool, +} + /// Main definition of editor keybindings #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Keybindings { @@ -260,6 +276,97 @@ impl KeySequenceState { Some(ReedlineEvent::Multiple(events)) } + pub fn process_combo_with_flush( + &mut self, + keybindings: &Keybindings, + combo: KeyCombination, + ) -> SequenceResolution { + if keybindings.sequence_bindings.is_empty() { + self.clear(); + return SequenceResolution { + combos: vec![combo], + ..SequenceResolution::default() + }; + } + + self.buffer.push(combo); + let mut resolution = SequenceResolution::default(); + + loop { + match keybindings.sequence_match(&self.buffer) { + KeySequenceMatch::Exact(event) => { + self.buffer.clear(); + self.pending_exact = None; + resolution.events.push(event); + break; + } + KeySequenceMatch::ExactAndPrefix(event) => { + self.pending_exact = Some((self.buffer.len(), event)); + resolution.pending = true; + break; + } + KeySequenceMatch::Prefix => { + self.pending_exact = None; + resolution.pending = true; + break; + } + KeySequenceMatch::NoMatch => { + if let Some((pending_len, pending_event)) = self.pending_exact.take() { + let remaining = if pending_len < self.buffer.len() { + self.buffer.split_off(pending_len) + } else { + Vec::new() + }; + self.buffer.clear(); + self.buffer = remaining; + resolution.events.push(pending_event); + if self.buffer.is_empty() { + break; + } + continue; + } + + let flushed = self.buffer.remove(0); + resolution.combos.push(flushed); + if self.buffer.is_empty() { + break; + } + } + } + } + + if self.buffer.is_empty() { + self.pending_exact = None; + } + + resolution.pending = !self.buffer.is_empty(); + resolution + } + + pub fn flush_with_combos(&mut self) -> SequenceResolution { + if self.buffer.is_empty() { + return SequenceResolution::default(); + } + + let mut resolution = SequenceResolution::default(); + + if let Some((pending_len, pending_event)) = self.pending_exact.take() { + let remaining = if pending_len < self.buffer.len() { + self.buffer.split_off(pending_len) + } else { + Vec::new() + }; + self.buffer.clear(); + resolution.events.push(pending_event); + resolution.combos = remaining; + } else { + resolution.combos = std::mem::take(&mut self.buffer); + } + + self.pending_exact = None; + resolution + } + pub fn flush(&mut self, mut fallback: F) -> Option where F: FnMut(KeyCombination) -> ReedlineEvent, diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index f83590f7..83355d94 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -12,7 +12,11 @@ use self::motion::ViCharSearch; use super::EditMode; use crate::{ - edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, + edit_mode::{ + keybindings::{Keybindings, SequenceResolution}, + vi::parser::parse, + KeyCombination, KeySequenceState, + }, enums::{EditCommand, EventStatus, ReedlineEvent, ReedlineRawEvent}, PromptEditMode, PromptViMode, }; @@ -42,7 +46,8 @@ pub struct Vi { cache: Vec, insert_keybindings: Keybindings, normal_keybindings: Keybindings, - insert_sequence_state: KeySequenceState, + visual_keybindings: Keybindings, + sequence_state: KeySequenceState, mode: ViMode, previous: Option, // last f, F, t, T motion for ; and , @@ -51,25 +56,46 @@ pub struct Vi { impl Default for Vi { fn default() -> Self { - Vi { - insert_keybindings: default_vi_insert_keybindings(), - normal_keybindings: default_vi_normal_keybindings(), - cache: Vec::new(), - insert_sequence_state: KeySequenceState::default(), - mode: ViMode::Insert, - previous: None, - last_char_search: None, - } + Self::new( + default_vi_insert_keybindings(), + default_vi_normal_keybindings(), + ) } } impl Vi { /// Creates Vi editor using defined keybindings pub fn new(insert_keybindings: Keybindings, normal_keybindings: Keybindings) -> Self { + let mut visual_keybindings = normal_keybindings.clone(); + visual_keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Esc, + ReedlineEvent::ViExitToNormalMode, + ); + let _ = visual_keybindings.remove_binding(KeyModifiers::NONE, KeyCode::Char('v')); + + Self::new_with_visual_keybindings( + insert_keybindings, + normal_keybindings, + visual_keybindings, + ) + } + + /// Creates Vi editor using defined keybindings, including visual mode + pub fn new_with_visual_keybindings( + insert_keybindings: Keybindings, + normal_keybindings: Keybindings, + visual_keybindings: Keybindings, + ) -> Self { Self { insert_keybindings, normal_keybindings, - ..Default::default() + visual_keybindings, + cache: Vec::new(), + sequence_state: KeySequenceState::default(), + mode: ViMode::Insert, + previous: None, + last_char_search: None, } } } @@ -79,79 +105,19 @@ impl EditMode for Vi { match event.into() { Event::Key(KeyEvent { code, modifiers, .. - }) => match (self.mode, modifiers, code) { - (ViMode::Normal, KeyModifiers::NONE, KeyCode::Char('v')) => { - self.cache.clear(); - self.mode = ViMode::Visual; - ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) - } - (ViMode::Normal | ViMode::Visual, modifier, KeyCode::Char(c)) => { - let c = c.to_ascii_lowercase(); - - if let Some(event) = self - .normal_keybindings - .find_binding(modifiers, KeyCode::Char(c)) - { - event - } else if modifier == KeyModifiers::NONE || modifier == KeyModifiers::SHIFT { - self.cache.push(if modifier == KeyModifiers::SHIFT { - c.to_ascii_uppercase() - } else { - c - }); - - let res = parse(&mut self.cache.iter().peekable()); - - if !res.is_valid() { - self.cache.clear(); - ReedlineEvent::None - } else if res.is_complete(self.mode) { - let event = res.to_reedline_event(self); - if let Some(mode) = res.changes_mode(self.mode) { - self.mode = mode; - } - self.cache.clear(); - event - } else { - ReedlineEvent::None - } - } else { - ReedlineEvent::None - } - } - (ViMode::Insert, modifier, KeyCode::Char(c)) => { - self.handle_insert_key(modifier, KeyCode::Char(c)) - } - (_, KeyModifiers::NONE, KeyCode::Esc) => { - self.cache.clear(); - self.mode = ViMode::Normal; - self.insert_sequence_state.clear(); - ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) - } - (ViMode::Normal | ViMode::Visual, _, _) => self - .normal_keybindings - .find_binding(modifiers, code) - .unwrap_or_else(|| { - // Default Enter behavior when no custom binding - if modifiers == KeyModifiers::NONE && code == KeyCode::Enter { - self.mode = ViMode::Insert; - ReedlineEvent::Enter - } else { - ReedlineEvent::None - } - }), - (ViMode::Insert, _, _) => self.handle_insert_key(modifiers, code), - }, + }) => self.handle_key_event(modifiers, code), - Event::Mouse(_) => self.with_flushed_insert_sequence(ReedlineEvent::Mouse), + Event::Mouse(_) => self.with_flushed_sequence(ReedlineEvent::Mouse), Event::Resize(width, height) => { - self.with_flushed_insert_sequence(ReedlineEvent::Resize(width, height)) + self.with_flushed_sequence(ReedlineEvent::Resize(width, height)) + } + Event::FocusGained => self.with_flushed_sequence(ReedlineEvent::None), + Event::FocusLost => self.with_flushed_sequence(ReedlineEvent::None), + Event::Paste(body) => { + self.with_flushed_sequence(ReedlineEvent::Edit(vec![EditCommand::InsertString( + body.replace("\r\n", "\n").replace('\r', "\n"), + )])) } - Event::FocusGained => self.with_flushed_insert_sequence(ReedlineEvent::None), - Event::FocusLost => self.with_flushed_insert_sequence(ReedlineEvent::None), - Event::Paste(body) => self.with_flushed_insert_sequence(ReedlineEvent::Edit(vec![ - EditCommand::InsertString(body.replace("\r\n", "\n").replace('\r', "\n")), - ])), } } @@ -164,34 +130,32 @@ impl EditMode for Vi { fn handle_mode_specific_event(&mut self, event: ReedlineEvent) -> EventStatus { match event { - ReedlineEvent::ViChangeMode(mode_str) => match ViMode::from_str(&mode_str) { - Ok(mode) => { - self.mode = mode; - self.insert_sequence_state.clear(); - EventStatus::Handled - } - Err(_) => EventStatus::Inapplicable, - }, + ReedlineEvent::ViChangeMode(mode_str) => ViMode::from_str(&mode_str) + .map(|mode| self.set_mode(mode)) + .unwrap_or(EventStatus::Inapplicable), + ReedlineEvent::ViExitToNormalMode => self.set_mode(ViMode::Normal), _ => EventStatus::Inapplicable, } } fn has_pending_sequence(&self) -> bool { - matches!(self.mode, ViMode::Insert) && self.insert_sequence_state.is_pending() + self.sequence_state.is_pending() } fn flush_pending_sequence(&mut self) -> Option { - if !matches!(self.mode, ViMode::Insert) { - return None; - } - - let keybindings = &self.insert_keybindings; - self.insert_sequence_state - .flush(|combo| Self::insert_single_key_event(keybindings, combo)) + let resolution = self.sequence_state.flush_with_combos(); + self.resolve_sequence_resolution(resolution) } } impl Vi { + fn set_mode(&mut self, mode: ViMode) -> EventStatus { + self.mode = mode; + self.cache.clear(); + self.sequence_state.clear(); + EventStatus::Handled + } + fn normalize_key_combo(modifier: KeyModifiers, code: KeyCode) -> KeyCombination { let key_code = match code { KeyCode::Char(c) => { @@ -207,6 +171,57 @@ impl Vi { KeyCombination { modifier, key_code } } + fn handle_key_event(&mut self, modifier: KeyModifiers, code: KeyCode) -> ReedlineEvent { + let combo = Self::normalize_key_combo(modifier, code); + let keybindings = match self.mode { + ViMode::Normal => &self.normal_keybindings, + ViMode::Visual => &self.visual_keybindings, + ViMode::Insert => &self.insert_keybindings, + }; + let resolution = self + .sequence_state + .process_combo_with_flush(keybindings, combo); + + self.resolve_sequence_resolution(resolution) + .unwrap_or(ReedlineEvent::None) + } + + fn keybindings_for_mode(&self, mode: ViMode) -> &Keybindings { + match mode { + ViMode::Normal => &self.normal_keybindings, + ViMode::Visual => &self.visual_keybindings, + ViMode::Insert => &self.insert_keybindings, + } + } + + fn resolve_sequence_resolution( + &mut self, + resolution: SequenceResolution, + ) -> Option { + if resolution.pending && resolution.events.is_empty() && resolution.combos.is_empty() { + return None; + } + + let mut events = Vec::new(); + for event in resolution.events { + Self::append_event(&mut events, event); + } + + for combo in resolution.combos { + let event = self.single_key_event_without_sequences(combo); + Self::append_event(&mut events, event); + } + + Self::combine_events(events) + } + + fn single_key_event_without_sequences(&mut self, combo: KeyCombination) -> ReedlineEvent { + match self.mode { + ViMode::Insert => Self::insert_single_key_event(&self.insert_keybindings, combo), + ViMode::Normal | ViMode::Visual => self.normal_visual_single_key_event(combo), + } + } + fn insert_single_key_event(keybindings: &Keybindings, combo: KeyCombination) -> ReedlineEvent { match combo.key_code { KeyCode::Char(c) => keybindings @@ -241,17 +256,79 @@ impl Vi { } } - fn handle_insert_key(&mut self, modifier: KeyModifiers, code: KeyCode) -> ReedlineEvent { - let combo = Self::normalize_key_combo(modifier, code); - let keybindings = &self.insert_keybindings; - self.insert_sequence_state - .process_combo(keybindings, combo, |combo| { - Self::insert_single_key_event(keybindings, combo) - }) - .unwrap_or(ReedlineEvent::None) + fn normal_visual_single_key_event(&mut self, combo: KeyCombination) -> ReedlineEvent { + let mode = self.mode; + let keybindings = self.keybindings_for_mode(mode); + match combo.key_code { + KeyCode::Char(c) => { + let c = c.to_ascii_lowercase(); + + if let Some(event) = keybindings.find_binding(combo.modifier, KeyCode::Char(c)) { + return event; + } + + if combo.modifier == KeyModifiers::NONE || combo.modifier == KeyModifiers::SHIFT { + self.cache.push(if combo.modifier == KeyModifiers::SHIFT { + c.to_ascii_uppercase() + } else { + c + }); + + let res = parse(&mut self.cache.iter().peekable()); + + if !res.is_valid() { + self.cache.clear(); + ReedlineEvent::None + } else if res.is_complete(mode) { + let event = res.to_reedline_event(self); + if let Some(new_mode) = res.changes_mode(mode) { + self.mode = new_mode; + self.sequence_state.clear(); + } + self.cache.clear(); + event + } else { + ReedlineEvent::None + } + } else { + ReedlineEvent::None + } + } + code => keybindings + .find_binding(combo.modifier, code) + .unwrap_or_else(|| { + if combo.modifier == KeyModifiers::NONE && code == KeyCode::Enter { + self.mode = ViMode::Insert; + self.sequence_state.clear(); + ReedlineEvent::Enter + } else { + ReedlineEvent::None + } + }), + } + } + + fn append_event(events: &mut Vec, event: ReedlineEvent) { + match event { + ReedlineEvent::None => {} + ReedlineEvent::Multiple(mut inner) => events.append(&mut inner), + other => events.push(other), + } + } + + fn combine_events(mut events: Vec) -> Option { + if events.is_empty() { + return None; + } + + if events.len() == 1 { + return Some(events.remove(0)); + } + + Some(ReedlineEvent::Multiple(events)) } - fn with_flushed_insert_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { + fn with_flushed_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { let Some(flush_event) = self.flush_pending_sequence() else { return event; }; @@ -277,18 +354,28 @@ mod test { use pretty_assertions::assert_eq; #[test] - fn esc_leads_to_normal_mode_test() { + fn esc_in_insert_emits_exit_to_normal() { let mut vi = Vi::default(); let esc = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))) .unwrap(); let result = vi.parse_event(esc); - assert_eq!( - result, - ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) - ); - assert!(matches!(vi.mode, ViMode::Normal)); + assert_eq!(result, ReedlineEvent::ViExitToNormalMode); + } + + #[test] + fn esc_in_normal_repaints() { + let mut vi = Vi { + mode: ViMode::Normal, + ..Default::default() + }; + let esc = + ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))) + .unwrap(); + let result = vi.parse_event(esc); + + assert_eq!(result, ReedlineEvent::Repaint); } #[test] @@ -392,11 +479,7 @@ mod test { #[test] fn insert_sequence_binding_emits_event() { let mut insert_keybindings = default_vi_insert_keybindings(); - let exit_event = ReedlineEvent::Multiple(vec![ - ReedlineEvent::Esc, - ReedlineEvent::ViChangeMode("normal".to_string()), - ReedlineEvent::Repaint, - ]); + let exit_event = ReedlineEvent::ViExitToNormalMode; insert_keybindings.add_sequence_binding( vec![ KeyCombination { diff --git a/src/edit_mode/vi/vi_keybindings.rs b/src/edit_mode/vi/vi_keybindings.rs index 20cca81a..25988f79 100644 --- a/src/edit_mode/vi/vi_keybindings.rs +++ b/src/edit_mode/vi/vi_keybindings.rs @@ -8,7 +8,7 @@ use crate::{ }, Keybindings, }, - EditCommand, + EditCommand, ReedlineEvent, }; /// Default Vi normal keybindings @@ -28,6 +28,16 @@ pub fn default_vi_normal_keybindings() -> Keybindings { edit_bind(EC::MoveLeft { select: false }), ); kb.add_binding(KM::NONE, KC::Delete, edit_bind(EC::Delete)); + kb.add_binding(KM::NONE, KC::Esc, ReedlineEvent::Repaint); + kb.add_binding( + KM::NONE, + KC::Char('v'), + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Esc, + ReedlineEvent::ViChangeMode("visual".to_string()), + ReedlineEvent::Repaint, + ]), + ); kb } @@ -35,11 +45,14 @@ pub fn default_vi_normal_keybindings() -> Keybindings { /// Default Vi insert keybindings pub fn default_vi_insert_keybindings() -> Keybindings { let mut kb = Keybindings::new(); + use KeyCode as KC; + use KeyModifiers as KM; add_common_control_bindings(&mut kb); add_common_navigation_bindings(&mut kb); add_common_edit_bindings(&mut kb); add_common_selection_bindings(&mut kb); + kb.add_binding(KM::NONE, KC::Esc, ReedlineEvent::ViExitToNormalMode); kb } diff --git a/src/engine.rs b/src/engine.rs index f1be8025..da2c1b54 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -28,7 +28,7 @@ use { HistoryNavigationQuery, HistorySessionId, SearchDirection, SearchQuery, }, painting::{Painter, PainterSuspendedState, PromptLines}, - prompt::{PromptEditMode, PromptHistorySearchStatus}, + prompt::{PromptEditMode, PromptHistorySearchStatus, PromptViMode}, result::{ReedlineError, ReedlineErrorVariants}, terminal_extensions::{bracketed_paste::BracketedPasteGuard, kitty::KittyProtocolGuard}, utils::text_manipulation, @@ -989,6 +989,13 @@ impl Reedline { self.input_mode = InputMode::Regular; Ok(EventStatus::Handled) } + ReedlineEvent::ViExitToNormalMode => { + self.input_mode = InputMode::Regular; + let _ = self + .edit_mode + .handle_mode_specific_event(ReedlineEvent::ViExitToNormalMode); + Ok(EventStatus::Handled) + } // TODO: Check if events should be handled ReedlineEvent::Right | ReedlineEvent::Left @@ -1150,6 +1157,21 @@ impl Reedline { self.editor.clear_selection(); Ok(EventStatus::Handled) } + ReedlineEvent::ViExitToNormalMode => { + self.deactivate_menus(); + self.editor.clear_selection(); + let was_insert = matches!( + self.edit_mode.edit_mode(), + PromptEditMode::Vi(PromptViMode::Insert) + ); + let _ = self + .edit_mode + .handle_mode_specific_event(ReedlineEvent::ViExitToNormalMode); + if was_insert && self.editor.insertion_point() > 0 { + self.run_edit_commands(&[EditCommand::MoveLeft { select: false }]); + } + Ok(EventStatus::Handled) + } ReedlineEvent::CtrlD => { if self.editor.is_empty() { self.editor.reset_undo_stack(); diff --git a/src/enums.rs b/src/enums.rs index 3f1d9181..11bc4c56 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -833,6 +833,9 @@ pub enum ReedlineEvent { /// Change mode (vi mode only) ViChangeMode(String), + + /// Exit vi insert/visual mode to normal + ViExitToNormalMode, } impl Display for ReedlineEvent { @@ -877,6 +880,7 @@ impl Display for ReedlineEvent { ReedlineEvent::ExecuteHostCommand(_) => write!(f, "ExecuteHostCommand"), ReedlineEvent::OpenEditor => write!(f, "OpenEditor"), ReedlineEvent::ViChangeMode(_) => write!(f, "ViChangeMode mode: "), + ReedlineEvent::ViExitToNormalMode => write!(f, "ViExitToNormalMode"), } } } diff --git a/src/lib.rs b/src/lib.rs index 5b5441e8..985fdcbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,11 +84,7 @@ //! key_code: KeyCode::Char('j'), //! }, //! ], -//! ReedlineEvent::Multiple(vec![ -//! ReedlineEvent::Esc, -//! ReedlineEvent::ViChangeMode("normal".into()), -//! ReedlineEvent::Repaint, -//! ]), +//! ReedlineEvent::ViExitToNormalMode, //! ); //! //! let edit_mode = Box::new(Vi::new( From 9d8ea67a2abbaf99dc5396817e444b6c023652eb Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 13:18:07 -0500 Subject: [PATCH 3/8] fix f/t motions with space --- src/core_editor/line_buffer.rs | 3 + src/edit_mode/vi/mod.rs | 146 ++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index 7886b4fb..dff9fde6 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -1445,6 +1445,7 @@ mod test { } #[rstest] + #[case("abc def", 0, ' ', true, 3)] #[case("abc def ghi", 0, 'c', true, 2)] #[case("abc def ghi", 0, 'a', true, 0)] #[case("abc def ghi", 0, 'z', true, 0)] @@ -1471,6 +1472,7 @@ mod test { } #[rstest] + #[case("abc def", 0, ' ', true, 2)] #[case("abc def ghi", 0, 'd', true, 3)] #[case("abc def ghi", 3, 'd', true, 3)] #[case("a😇c", 0, 'c', true, 1)] @@ -1513,6 +1515,7 @@ mod test { } #[rstest] + #[case("abc def", 0, ' ', true, " def")] #[case("abc def ghi", 0, 'b', true, "bc def ghi")] #[case("abc def ghi", 0, 'i', true, "i")] #[case("abc def ghi", 0, 'z', true, "abc def ghi")] diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 83355d94..7c5990e3 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -172,6 +172,14 @@ impl Vi { } fn handle_key_event(&mut self, modifier: KeyModifiers, code: KeyCode) -> ReedlineEvent { + if matches!(self.mode, ViMode::Normal | ViMode::Visual) + && !self.cache.is_empty() + && matches!(code, KeyCode::Char(_)) + { + let combo = Self::normalize_key_combo(modifier, code); + return self.normal_visual_single_key_event(combo); + } + let combo = Self::normalize_key_combo(modifier, code); let keybindings = match self.mode { ViMode::Normal => &self.normal_keybindings, @@ -259,12 +267,16 @@ impl Vi { fn normal_visual_single_key_event(&mut self, combo: KeyCombination) -> ReedlineEvent { let mode = self.mode; let keybindings = self.keybindings_for_mode(mode); + let cache_pending = !self.cache.is_empty(); match combo.key_code { KeyCode::Char(c) => { let c = c.to_ascii_lowercase(); - if let Some(event) = keybindings.find_binding(combo.modifier, KeyCode::Char(c)) { - return event; + if !cache_pending { + if let Some(event) = keybindings.find_binding(combo.modifier, KeyCode::Char(c)) + { + return event; + } } if combo.modifier == KeyModifiers::NONE || combo.modifier == KeyModifiers::SHIFT { @@ -350,7 +362,7 @@ impl Vi { #[cfg(test)] mod test { use super::*; - use crate::KeyCombination; + use crate::{EditCommand, KeyCombination}; use pretty_assertions::assert_eq; #[test] @@ -515,4 +527,132 @@ mod test { assert_eq!(vi.parse_event(first), ReedlineEvent::None); assert_eq!(vi.parse_event(second), exit_event); } + + #[test] + fn normal_mode_f_space_moves_to_space() { + let mut vi = Vi { + mode: ViMode::Normal, + ..Default::default() + }; + + let f = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char('f'), + KeyModifiers::NONE, + ))) + .unwrap(); + let space = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char(' '), + KeyModifiers::NONE, + ))) + .unwrap(); + + assert_eq!(vi.parse_event(f), ReedlineEvent::None); + assert_eq!( + vi.parse_event(space), + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveRightUntil { + c: ' ', + select: false, + }, + ])]) + ); + } + + #[test] + fn normal_mode_t_space_moves_before_space() { + let mut vi = Vi { + mode: ViMode::Normal, + ..Default::default() + }; + + let t = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char('t'), + KeyModifiers::NONE, + ))) + .unwrap(); + let space = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char(' '), + KeyModifiers::NONE, + ))) + .unwrap(); + + assert_eq!(vi.parse_event(t), ReedlineEvent::None); + assert_eq!( + vi.parse_event(space), + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveRightBefore { + c: ' ', + select: false, + }, + ])]) + ); + } + + #[test] + fn normal_mode_dt_space_cuts_before_space() { + let mut vi = Vi { + mode: ViMode::Normal, + ..Default::default() + }; + + let d = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char('d'), + KeyModifiers::NONE, + ))) + .unwrap(); + let t = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char('t'), + KeyModifiers::NONE, + ))) + .unwrap(); + let space = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char(' '), + KeyModifiers::NONE, + ))) + .unwrap(); + + assert_eq!(vi.parse_event(d), ReedlineEvent::None); + assert_eq!(vi.parse_event(t), ReedlineEvent::None); + assert_eq!( + vi.parse_event(space), + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::CutRightBefore(' ') + ])]) + ); + } + + #[test] + fn pending_motion_ignores_space_binding() { + let mut normal_keybindings = default_vi_normal_keybindings(); + normal_keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Char(' '), + ReedlineEvent::Edit(vec![EditCommand::InsertChar(' ')]), + ); + + let mut vi = Vi::new(default_vi_insert_keybindings(), normal_keybindings); + vi.mode = ViMode::Normal; + + let f = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char('f'), + KeyModifiers::NONE, + ))) + .unwrap(); + let space = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new( + KeyCode::Char(' '), + KeyModifiers::NONE, + ))) + .unwrap(); + + assert_eq!(vi.parse_event(f), ReedlineEvent::None); + assert_eq!( + vi.parse_event(space), + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![ + EditCommand::MoveRightUntil { + c: ' ', + select: false, + }, + ])]) + ); + } } From 0433666423656bbda5d4cb7de534b8d8dcd8aadb Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 15:52:30 -0500 Subject: [PATCH 4/8] greatly simplify original implementation --- src/edit_mode/emacs.rs | 13 +- src/edit_mode/keybindings.rs | 302 +++++++++++++---------------------- src/edit_mode/vi/mod.rs | 54 +------ 3 files changed, 126 insertions(+), 243 deletions(-) diff --git a/src/edit_mode/emacs.rs b/src/edit_mode/emacs.rs index b7c88c37..84d93d2a 100644 --- a/src/edit_mode/emacs.rs +++ b/src/edit_mode/emacs.rs @@ -126,10 +126,11 @@ impl EditMode for Emacs { }) => { let combo = Self::normalize_key_combo(modifiers, code); let keybindings = &self.keybindings; - self.sequence_state - .process_combo(keybindings, combo, |combo| { - Self::single_key_event(keybindings, combo) - }) + let resolution = self + .sequence_state + .process_combo(keybindings, combo); + resolution + .into_event(|combo| Self::single_key_event(keybindings, combo)) .unwrap_or(ReedlineEvent::None) } @@ -157,8 +158,8 @@ impl EditMode for Emacs { fn flush_pending_sequence(&mut self) -> Option { let keybindings = &self.keybindings; - self.sequence_state - .flush(|combo| Self::single_key_event(keybindings, combo)) + let resolution = self.sequence_state.flush_with_combos(); + resolution.into_event(|combo| Self::single_key_event(keybindings, combo)) } } diff --git a/src/edit_mode/keybindings.rs b/src/edit_mode/keybindings.rs index 45eaf903..25da902a 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -30,6 +30,9 @@ pub enum KeySequenceMatch { #[derive(Debug, Default, Clone)] pub struct KeySequenceState { buffer: Vec, + /// Stores an exact match that is also a prefix of a longer sequence. + /// If the longer sequence does not materialize, we emit this saved event + /// and continue processing any remaining buffered keys. pending_exact: Option<(usize, ReedlineEvent)>, } @@ -40,8 +43,24 @@ pub struct SequenceResolution { pub events: Vec, /// Key combinations to flush through fallback handling. pub combos: Vec, - /// Whether a sequence is pending further input. - pub pending: bool, +} + +impl SequenceResolution { + pub fn into_event(self, mut fallback: F) -> Option + where + F: FnMut(KeyCombination) -> ReedlineEvent, + { + let mut events = Vec::new(); + for event in self.events { + append_event(&mut events, event); + } + + for combo in self.combos { + append_event(&mut events, fallback(combo)); + } + + combine_events(events) + } } /// Main definition of editor keybindings @@ -50,7 +69,6 @@ pub struct Keybindings { /// Defines a keybinding for a reedline event pub bindings: HashMap, /// Defines a key sequence binding for a reedline event - #[serde(default)] pub sequence_bindings: HashMap, } @@ -69,11 +87,6 @@ impl Keybindings { } } - /// Defines an empty keybinding object - pub fn empty() -> Self { - Self::new() - } - /// Adds a keybinding /// /// # Panics @@ -197,86 +210,7 @@ impl KeySequenceState { self.pending_exact = None; } - pub fn process_combo( - &mut self, - keybindings: &Keybindings, - combo: KeyCombination, - mut fallback: F, - ) -> Option - where - F: FnMut(KeyCombination) -> ReedlineEvent, - { - if keybindings.sequence_bindings.is_empty() { - self.clear(); - return Some(fallback(combo)); - } - - self.buffer.push(combo); - - let mut events = Vec::new(); - - loop { - match keybindings.sequence_match(&self.buffer) { - KeySequenceMatch::Exact(event) => { - self.buffer.clear(); - self.pending_exact = None; - events.push(event); - break; - } - KeySequenceMatch::ExactAndPrefix(event) => { - self.pending_exact = Some((self.buffer.len(), event)); - break; - } - KeySequenceMatch::Prefix => { - self.pending_exact = None; - break; - } - KeySequenceMatch::NoMatch => { - if let Some((pending_len, pending_event)) = self.pending_exact.take() { - let remaining = if pending_len < self.buffer.len() { - self.buffer.split_off(pending_len) - } else { - Vec::new() - }; - self.buffer.clear(); - self.buffer = remaining; - events.push(pending_event); - if self.buffer.is_empty() { - break; - } - continue; - } - - let flushed = self.buffer.remove(0); - let event = fallback(flushed); - if !matches!(event, ReedlineEvent::None) { - events.push(event); - } - - if self.buffer.is_empty() { - break; - } - } - } - } - - let mut events: Vec = events - .into_iter() - .filter(|event| !matches!(event, ReedlineEvent::None)) - .collect(); - - if events.is_empty() { - return None; - } - - if events.len() == 1 { - return Some(events.remove(0)); - } - - Some(ReedlineEvent::Multiple(events)) - } - - pub fn process_combo_with_flush( + pub fn process_combo( &mut self, keybindings: &Keybindings, combo: KeyCombination, @@ -293,53 +227,15 @@ impl KeySequenceState { let mut resolution = SequenceResolution::default(); loop { - match keybindings.sequence_match(&self.buffer) { - KeySequenceMatch::Exact(event) => { - self.buffer.clear(); - self.pending_exact = None; - resolution.events.push(event); - break; - } - KeySequenceMatch::ExactAndPrefix(event) => { - self.pending_exact = Some((self.buffer.len(), event)); - resolution.pending = true; - break; - } - KeySequenceMatch::Prefix => { - self.pending_exact = None; - resolution.pending = true; - break; - } - KeySequenceMatch::NoMatch => { - if let Some((pending_len, pending_event)) = self.pending_exact.take() { - let remaining = if pending_len < self.buffer.len() { - self.buffer.split_off(pending_len) - } else { - Vec::new() - }; - self.buffer.clear(); - self.buffer = remaining; - resolution.events.push(pending_event); - if self.buffer.is_empty() { - break; - } - continue; - } - - let flushed = self.buffer.remove(0); - resolution.combos.push(flushed); - if self.buffer.is_empty() { - break; - } - } + match self.process_step(keybindings, &mut resolution) { + StepOutcome::Continue => continue, + StepOutcome::EmitDone | StepOutcome::Pending | StepOutcome::Done => break, } } if self.buffer.is_empty() { self.pending_exact = None; } - - resolution.pending = !self.buffer.is_empty(); resolution } @@ -350,16 +246,7 @@ impl KeySequenceState { let mut resolution = SequenceResolution::default(); - if let Some((pending_len, pending_event)) = self.pending_exact.take() { - let remaining = if pending_len < self.buffer.len() { - self.buffer.split_off(pending_len) - } else { - Vec::new() - }; - self.buffer.clear(); - resolution.events.push(pending_event); - resolution.combos = remaining; - } else { + if !self.flush_pending_exact(&mut resolution) { resolution.combos = std::mem::take(&mut self.buffer); } @@ -367,54 +254,73 @@ impl KeySequenceState { resolution } - pub fn flush(&mut self, mut fallback: F) -> Option - where - F: FnMut(KeyCombination) -> ReedlineEvent, - { - if self.buffer.is_empty() { - self.pending_exact = None; - return None; - } + fn flush_pending_exact(&mut self, resolution: &mut SequenceResolution) -> bool { + let Some((pending_len, pending_event)) = self.pending_exact.take() else { + return false; + }; - if let Some((pending_len, pending_event)) = self.pending_exact.take() { - let remaining = if pending_len < self.buffer.len() { - self.buffer.split_off(pending_len) - } else { - Vec::new() - }; - self.buffer.clear(); + let pending_len = pending_len.min(self.buffer.len()); + self.buffer.drain(..pending_len); + resolution.events.push(pending_event); + true + } - let mut events = vec![pending_event]; - for combo in remaining { - let event = fallback(combo); - if !matches!(event, ReedlineEvent::None) { - events.push(event); - } + fn process_step( + &mut self, + keybindings: &Keybindings, + resolution: &mut SequenceResolution, + ) -> StepOutcome { + match keybindings.sequence_match(&self.buffer) { + KeySequenceMatch::Exact(event) => { + self.buffer.clear(); + self.pending_exact = None; + resolution.events.push(event); + StepOutcome::EmitDone } - - return match events.len() { - 0 => None, - 1 => Some(events.remove(0)), - _ => Some(ReedlineEvent::Multiple(events)), - }; - } - - let mut events = Vec::new(); - for combo in self.buffer.drain(..) { - let event = fallback(combo); - if !matches!(event, ReedlineEvent::None) { - events.push(event); + KeySequenceMatch::ExactAndPrefix(event) => { + self.pending_exact = Some((self.buffer.len(), event)); + StepOutcome::Pending } - } + KeySequenceMatch::Prefix => { + self.pending_exact = None; + StepOutcome::Pending + } + // User input does not match any sequence; flush buffered keys. + KeySequenceMatch::NoMatch => { + // If we previously saw an exact match that was also a prefix, emit it now + // and keep any trailing keys for further processing. + if self.flush_pending_exact(resolution) { + if self.buffer.is_empty() { + return StepOutcome::Done; + } + return StepOutcome::Continue; + } - match events.len() { - 0 => None, - 1 => Some(events.remove(0)), - _ => Some(ReedlineEvent::Multiple(events)), + // Otherwise, drop the oldest key and replay it through fallback handling. + // NOTE: This is O(n), but sequence buffers are expected to be short. + let flushed = self.buffer.remove(0); + resolution.combos.push(flushed); + if self.buffer.is_empty() { + StepOutcome::Done + } else { + StepOutcome::Continue + } + } } } } +enum StepOutcome { + /// Shifted buffered input; continue resolving remaining keys. + Continue, + /// Current buffer is a valid prefix; wait for more input. + Pending, + /// Emitted an exact match; stop processing this step. + EmitDone, + /// Buffer emptied with no further input to process. + Done, +} + pub fn edit_bind(command: EditCommand) -> ReedlineEvent { ReedlineEvent::Edit(vec![command]) } @@ -621,6 +527,26 @@ pub fn add_common_selection_bindings(kb: &mut Keybindings) { ); } +fn append_event(events: &mut Vec, event: ReedlineEvent) { + match event { + ReedlineEvent::None => {} + ReedlineEvent::Multiple(mut inner) => events.append(&mut inner), + other => events.push(other), + } +} + +fn combine_events(mut events: Vec) -> Option { + if events.is_empty() { + return None; + } + + if events.len() == 1 { + return Some(events.remove(0)); + } + + Some(ReedlineEvent::Multiple(events)) +} + #[cfg(test)] mod tests { use super::*; @@ -661,11 +587,11 @@ mod tests { keybindings.add_sequence_binding(vec![combo('j'), combo('j')], ReedlineEvent::Esc); let mut state = KeySequenceState::default(); - let first = state.process_combo(&keybindings, combo('j'), fallback); - assert_eq!(first, None); + let first = state.process_combo(&keybindings, combo('j')); + assert_eq!(first.into_event(fallback), None); - let second = state.process_combo(&keybindings, combo('j'), fallback); - assert_eq!(second, Some(ReedlineEvent::Esc)); + let second = state.process_combo(&keybindings, combo('j')); + assert_eq!(second.into_event(fallback), Some(ReedlineEvent::Esc)); } #[test] @@ -674,11 +600,11 @@ mod tests { keybindings.add_sequence_binding(vec![combo('j'), combo('j')], ReedlineEvent::Esc); let mut state = KeySequenceState::default(); - let _ = state.process_combo(&keybindings, combo('j'), fallback); - let second = state.process_combo(&keybindings, combo('k'), fallback); + let _ = state.process_combo(&keybindings, combo('j')); + let second = state.process_combo(&keybindings, combo('k')); assert_eq!( - second, + second.into_event(fallback), Some(ReedlineEvent::Multiple(vec![ ReedlineEvent::Edit(vec![EditCommand::InsertChar('j')]), ReedlineEvent::Edit(vec![EditCommand::InsertChar('k')]), @@ -692,11 +618,11 @@ mod tests { keybindings.add_sequence_binding(vec![combo('j'), combo('j')], ReedlineEvent::Esc); let mut state = KeySequenceState::default(); - let _ = state.process_combo(&keybindings, combo('j'), fallback); - let flushed = state.flush(fallback); + let _ = state.process_combo(&keybindings, combo('j')); + let flushed = state.flush_with_combos(); assert_eq!( - flushed, + flushed.into_event(fallback), Some(ReedlineEvent::Edit(vec![EditCommand::InsertChar('j')])) ); } diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 7c5990e3..f858c648 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -12,11 +12,7 @@ use self::motion::ViCharSearch; use super::EditMode; use crate::{ - edit_mode::{ - keybindings::{Keybindings, SequenceResolution}, - vi::parser::parse, - KeyCombination, KeySequenceState, - }, + edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, enums::{EditCommand, EventStatus, ReedlineEvent, ReedlineRawEvent}, PromptEditMode, PromptViMode, }; @@ -144,7 +140,7 @@ impl EditMode for Vi { fn flush_pending_sequence(&mut self) -> Option { let resolution = self.sequence_state.flush_with_combos(); - self.resolve_sequence_resolution(resolution) + resolution.into_event(|combo| self.single_key_event_without_sequences(combo)) } } @@ -188,9 +184,10 @@ impl Vi { }; let resolution = self .sequence_state - .process_combo_with_flush(keybindings, combo); + .process_combo(keybindings, combo); - self.resolve_sequence_resolution(resolution) + resolution + .into_event(|combo| self.single_key_event_without_sequences(combo)) .unwrap_or(ReedlineEvent::None) } @@ -202,27 +199,6 @@ impl Vi { } } - fn resolve_sequence_resolution( - &mut self, - resolution: SequenceResolution, - ) -> Option { - if resolution.pending && resolution.events.is_empty() && resolution.combos.is_empty() { - return None; - } - - let mut events = Vec::new(); - for event in resolution.events { - Self::append_event(&mut events, event); - } - - for combo in resolution.combos { - let event = self.single_key_event_without_sequences(combo); - Self::append_event(&mut events, event); - } - - Self::combine_events(events) - } - fn single_key_event_without_sequences(&mut self, combo: KeyCombination) -> ReedlineEvent { match self.mode { ViMode::Insert => Self::insert_single_key_event(&self.insert_keybindings, combo), @@ -320,26 +296,6 @@ impl Vi { } } - fn append_event(events: &mut Vec, event: ReedlineEvent) { - match event { - ReedlineEvent::None => {} - ReedlineEvent::Multiple(mut inner) => events.append(&mut inner), - other => events.push(other), - } - } - - fn combine_events(mut events: Vec) -> Option { - if events.is_empty() { - return None; - } - - if events.len() == 1 { - return Some(events.remove(0)); - } - - Some(ReedlineEvent::Multiple(events)) - } - fn with_flushed_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { let Some(flush_event) = self.flush_pending_sequence() else { return event; From d28daf470c590d18d7b0fa9044db9af244133526 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 15:57:07 -0500 Subject: [PATCH 5/8] satisfy clippy with a `while let` --- src/edit_mode/keybindings.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/edit_mode/keybindings.rs b/src/edit_mode/keybindings.rs index 25da902a..0912a18d 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -226,12 +226,7 @@ impl KeySequenceState { self.buffer.push(combo); let mut resolution = SequenceResolution::default(); - loop { - match self.process_step(keybindings, &mut resolution) { - StepOutcome::Continue => continue, - StepOutcome::EmitDone | StepOutcome::Pending | StepOutcome::Done => break, - } - } + while let StepOutcome::Continue = self.process_step(keybindings, &mut resolution) { } if self.buffer.is_empty() { self.pending_exact = None; From d876438a72676d05307537786fd8461bfc70b1c7 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 16:48:30 -0500 Subject: [PATCH 6/8] reduce duplication across Vi and Emacs editors --- src/edit_mode/base.rs | 95 ++++++++++++++++++++++++- src/edit_mode/emacs.rs | 104 ++++----------------------- src/edit_mode/keybindings.rs | 40 +++++++++-- src/edit_mode/vi/mod.rs | 134 ++++++----------------------------- 4 files changed, 163 insertions(+), 210 deletions(-) diff --git a/src/edit_mode/base.rs b/src/edit_mode/base.rs index dd04c0de..28575acd 100644 --- a/src/edit_mode/base.rs +++ b/src/edit_mode/base.rs @@ -1,7 +1,10 @@ use crate::{ enums::{EventStatus, ReedlineEvent, ReedlineRawEvent}, - PromptEditMode, + EditCommand, PromptEditMode, }; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; + +use super::{keybindings, KeyCombination}; /// Define the style of parsing for the edit events /// Available default options: @@ -9,7 +12,56 @@ use crate::{ /// - Vi pub trait EditMode: Send { /// Translate the given user input event into what the `LineEditor` understands - fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent; + fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent { + match event.into() { + Event::Key(KeyEvent { + code, modifiers, .. + }) => self.parse_key_event(modifiers, code), + other => self.parse_non_key_event(other), + } + } + + /// Translate key events into what the `LineEditor` understands + fn parse_key_event(&mut self, modifiers: KeyModifiers, code: KeyCode) -> ReedlineEvent; + + /// Resolve a key combination using keybindings with a fallback to insertable characters. + fn default_key_event( + &self, + keybindings: &keybindings::Keybindings, + combo: KeyCombination, + ) -> ReedlineEvent { + match combo.key_code { + KeyCode::Char(c) => keybindings + .find_binding(combo.modifier, KeyCode::Char(c)) + .unwrap_or_else(|| { + if combo.modifier == KeyModifiers::NONE + || combo.modifier == KeyModifiers::SHIFT + || combo.modifier == KeyModifiers::CONTROL | KeyModifiers::ALT + || combo.modifier + == KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT + { + ReedlineEvent::Edit(vec![EditCommand::InsertChar( + if combo.modifier == KeyModifiers::SHIFT { + c.to_ascii_uppercase() + } else { + c + }, + )]) + } else { + ReedlineEvent::None + } + }), + code => keybindings + .find_binding(combo.modifier, code) + .unwrap_or_else(|| { + if combo.modifier == KeyModifiers::NONE && code == KeyCode::Enter { + ReedlineEvent::Enter + } else { + ReedlineEvent::None + } + }), + } + } /// What to display in the prompt indicator fn edit_mode(&self) -> PromptEditMode; @@ -19,6 +71,45 @@ pub trait EditMode: Send { EventStatus::Inapplicable } + /// Translate non-key events into what the `LineEditor` understands + fn parse_non_key_event(&mut self, event: Event) -> ReedlineEvent { + match event { + Event::Key(KeyEvent { + code, modifiers, .. + }) => self.parse_key_event(modifiers, code), + Event::Mouse(_) => self.with_flushed_sequence(ReedlineEvent::Mouse), + Event::Resize(width, height) => { + self.with_flushed_sequence(ReedlineEvent::Resize(width, height)) + } + Event::FocusGained => self.with_flushed_sequence(ReedlineEvent::None), + Event::FocusLost => self.with_flushed_sequence(ReedlineEvent::None), + Event::Paste(body) => { + self.with_flushed_sequence(ReedlineEvent::Edit(vec![EditCommand::InsertString( + body.replace("\r\n", "\n").replace('\r', "\n"), + )])) + } + } + } + + /// Flush pending sequences and combine them with an incoming event. + fn with_flushed_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { + let Some(flush_event) = self.flush_pending_sequence() else { + return event; + }; + + if matches!(event, ReedlineEvent::None) { + return flush_event; + } + + match flush_event { + ReedlineEvent::Multiple(mut events) => { + events.push(event); + ReedlineEvent::Multiple(events) + } + other => ReedlineEvent::Multiple(vec![other, event]), + } + } + /// Whether a key sequence is currently pending fn has_pending_sequence(&self) -> bool { false diff --git a/src/edit_mode/emacs.rs b/src/edit_mode/emacs.rs index 84d93d2a..2a5dadab 100644 --- a/src/edit_mode/emacs.rs +++ b/src/edit_mode/emacs.rs @@ -7,10 +7,11 @@ use crate::{ }, EditMode, }, - enums::{EditCommand, ReedlineEvent, ReedlineRawEvent}, + enums::{EditCommand, ReedlineEvent}, PromptEditMode, }; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{KeyCode, KeyModifiers}; + /// Returns the current default emacs keybindings pub fn default_emacs_keybindings() -> Keybindings { @@ -119,33 +120,13 @@ impl Default for Emacs { } impl EditMode for Emacs { - fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent { - match event.into() { - Event::Key(KeyEvent { - code, modifiers, .. - }) => { - let combo = Self::normalize_key_combo(modifiers, code); - let keybindings = &self.keybindings; - let resolution = self - .sequence_state - .process_combo(keybindings, combo); - resolution - .into_event(|combo| Self::single_key_event(keybindings, combo)) - .unwrap_or(ReedlineEvent::None) - } - - Event::Mouse(_) => self.with_flushed_sequence(ReedlineEvent::Mouse), - Event::Resize(width, height) => { - self.with_flushed_sequence(ReedlineEvent::Resize(width, height)) - } - Event::FocusGained => self.with_flushed_sequence(ReedlineEvent::None), - Event::FocusLost => self.with_flushed_sequence(ReedlineEvent::None), - Event::Paste(body) => { - self.with_flushed_sequence(ReedlineEvent::Edit(vec![EditCommand::InsertString( - body.replace("\r\n", "\n").replace('\r', "\n"), - )])) - } - } + fn parse_key_event(&mut self, modifiers: KeyModifiers, code: KeyCode) -> ReedlineEvent { + let combo = KeyCombination::from((modifiers, code)); + let keybindings = &self.keybindings; + let resolution = self.sequence_state.process_combo(keybindings, combo); + resolution + .into_event(|combo| self.default_key_event(keybindings, combo)) + .unwrap_or(ReedlineEvent::None) } fn edit_mode(&self) -> PromptEditMode { @@ -159,7 +140,7 @@ impl EditMode for Emacs { fn flush_pending_sequence(&mut self) -> Option { let keybindings = &self.keybindings; let resolution = self.sequence_state.flush_with_combos(); - resolution.into_event(|combo| Self::single_key_event(keybindings, combo)) + resolution.into_event(|combo| self.default_key_event(keybindings, combo)) } } @@ -171,72 +152,13 @@ impl Emacs { sequence_state: KeySequenceState::new(), } } - - fn normalize_key_combo(modifier: KeyModifiers, code: KeyCode) -> KeyCombination { - let key_code = match code { - KeyCode::Char(c) => { - let c = match modifier { - KeyModifiers::NONE => c, - _ => c.to_ascii_lowercase(), - }; - KeyCode::Char(c) - } - other => other, - }; - - KeyCombination { modifier, key_code } - } - - fn single_key_event(keybindings: &Keybindings, combo: KeyCombination) -> ReedlineEvent { - match combo.key_code { - KeyCode::Char(c) => keybindings - .find_binding(combo.modifier, KeyCode::Char(c)) - .unwrap_or_else(|| { - if combo.modifier == KeyModifiers::NONE - || combo.modifier == KeyModifiers::SHIFT - || combo.modifier == KeyModifiers::CONTROL | KeyModifiers::ALT - || combo.modifier - == KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT - { - ReedlineEvent::Edit(vec![EditCommand::InsertChar( - if combo.modifier == KeyModifiers::SHIFT { - c.to_ascii_uppercase() - } else { - c - }, - )]) - } else { - ReedlineEvent::None - } - }), - code => keybindings - .find_binding(combo.modifier, code) - .unwrap_or(ReedlineEvent::None), - } - } - - fn with_flushed_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { - let Some(flush_event) = self.flush_pending_sequence() else { - return event; - }; - - if matches!(event, ReedlineEvent::None) { - return flush_event; - } - - match flush_event { - ReedlineEvent::Multiple(mut events) => { - events.push(event); - ReedlineEvent::Multiple(events) - } - other => ReedlineEvent::Multiple(vec![other, event]), - } - } } #[cfg(test)] mod test { use super::*; + use crate::enums::ReedlineRawEvent; + use crossterm::event::{Event, KeyEvent}; use pretty_assertions::assert_eq; #[test] diff --git a/src/edit_mode/keybindings.rs b/src/edit_mode/keybindings.rs index 0912a18d..a815d71f 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -14,10 +14,39 @@ pub struct KeyCombination { pub key_code: KeyCode, } +impl From<(KeyModifiers, KeyCode)> for KeyCombination { + fn from((modifier, code): (KeyModifiers, KeyCode)) -> Self { + let key_code = match code { + KeyCode::Char(c) => { + let c = match modifier { + KeyModifiers::NONE => c, + _ => c.to_ascii_lowercase(), + }; + KeyCode::Char(c) + } + other => other, + }; + + Self { modifier, key_code } + } +} + /// Sequence of key combinations. #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] pub struct KeySequence(pub Vec); +impl From> for KeySequence { + fn from(sequence: Vec) -> Self { + Self(sequence) + } +} + +impl AsRef<[KeyCombination]> for KeySequence { + fn as_ref(&self) -> &[KeyCombination] { + &self.0 + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum KeySequenceMatch { Exact(ReedlineEvent), @@ -128,7 +157,7 @@ impl Keybindings { } self.sequence_bindings - .insert(KeySequence(sequence), command); + .insert(KeySequence::from(sequence), command); } /// Find a keybinding based on the modifier and keycode @@ -145,11 +174,12 @@ impl Keybindings { let exact = self .sequence_bindings - .get(&KeySequence(sequence.to_vec())) + .get(&KeySequence::from(sequence.to_vec())) .cloned(); let is_prefix = self.sequence_bindings.keys().any(|key_sequence| { - key_sequence.0.len() > sequence.len() && key_sequence.0[..sequence.len()] == *sequence + let keys = key_sequence.as_ref(); + keys.len() > sequence.len() && keys[..sequence.len()] == *sequence }); match (exact, is_prefix) { @@ -179,7 +209,7 @@ impl Keybindings { &mut self, sequence: Vec, ) -> Option { - self.sequence_bindings.remove(&KeySequence(sequence)) + self.sequence_bindings.remove(&KeySequence::from(sequence)) } /// Get assigned keybindings @@ -226,7 +256,7 @@ impl KeySequenceState { self.buffer.push(combo); let mut resolution = SequenceResolution::default(); - while let StepOutcome::Continue = self.process_step(keybindings, &mut resolution) { } + while let StepOutcome::Continue = self.process_step(keybindings, &mut resolution) {} if self.buffer.is_empty() { self.pending_exact = None; diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index f858c648..44a86837 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -5,7 +5,7 @@ mod vi_keybindings; use std::str::FromStr; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{KeyCode, KeyModifiers}; pub use vi_keybindings::{default_vi_insert_keybindings, default_vi_normal_keybindings}; use self::motion::ViCharSearch; @@ -13,7 +13,7 @@ use self::motion::ViCharSearch; use super::EditMode; use crate::{ edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, - enums::{EditCommand, EventStatus, ReedlineEvent, ReedlineRawEvent}, + enums::{EventStatus, ReedlineEvent}, PromptEditMode, PromptViMode, }; @@ -97,24 +97,24 @@ impl Vi { } impl EditMode for Vi { - fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent { - match event.into() { - Event::Key(KeyEvent { - code, modifiers, .. - }) => self.handle_key_event(modifiers, code), - - Event::Mouse(_) => self.with_flushed_sequence(ReedlineEvent::Mouse), - Event::Resize(width, height) => { - self.with_flushed_sequence(ReedlineEvent::Resize(width, height)) - } - Event::FocusGained => self.with_flushed_sequence(ReedlineEvent::None), - Event::FocusLost => self.with_flushed_sequence(ReedlineEvent::None), - Event::Paste(body) => { - self.with_flushed_sequence(ReedlineEvent::Edit(vec![EditCommand::InsertString( - body.replace("\r\n", "\n").replace('\r', "\n"), - )])) - } + fn parse_key_event(&mut self, modifier: KeyModifiers, code: KeyCode) -> ReedlineEvent { + let combo = KeyCombination::from((modifier, code)); + + // If a vi command is in-flight, force the next character through the parser + // so motions like f/t/dt+ (including space) are not intercepted by keybindings. + if matches!(self.mode, ViMode::Normal | ViMode::Visual) + && !self.cache.is_empty() + && matches!(code, KeyCode::Char(_)) + { + return self.normal_visual_single_key_event(combo); } + + let keybindings = &self.keybindings_for_mode(self.mode).clone(); + let resolution = self.sequence_state.process_combo(keybindings, combo); + + resolution + .into_event(|combo| self.single_key_event_without_sequences(combo)) + .unwrap_or(ReedlineEvent::None) } fn edit_mode(&self) -> PromptEditMode { @@ -152,45 +152,6 @@ impl Vi { EventStatus::Handled } - fn normalize_key_combo(modifier: KeyModifiers, code: KeyCode) -> KeyCombination { - let key_code = match code { - KeyCode::Char(c) => { - let c = match modifier { - KeyModifiers::NONE => c, - _ => c.to_ascii_lowercase(), - }; - KeyCode::Char(c) - } - other => other, - }; - - KeyCombination { modifier, key_code } - } - - fn handle_key_event(&mut self, modifier: KeyModifiers, code: KeyCode) -> ReedlineEvent { - if matches!(self.mode, ViMode::Normal | ViMode::Visual) - && !self.cache.is_empty() - && matches!(code, KeyCode::Char(_)) - { - let combo = Self::normalize_key_combo(modifier, code); - return self.normal_visual_single_key_event(combo); - } - - let combo = Self::normalize_key_combo(modifier, code); - let keybindings = match self.mode { - ViMode::Normal => &self.normal_keybindings, - ViMode::Visual => &self.visual_keybindings, - ViMode::Insert => &self.insert_keybindings, - }; - let resolution = self - .sequence_state - .process_combo(keybindings, combo); - - resolution - .into_event(|combo| self.single_key_event_without_sequences(combo)) - .unwrap_or(ReedlineEvent::None) - } - fn keybindings_for_mode(&self, mode: ViMode) -> &Keybindings { match mode { ViMode::Normal => &self.normal_keybindings, @@ -201,45 +162,11 @@ impl Vi { fn single_key_event_without_sequences(&mut self, combo: KeyCombination) -> ReedlineEvent { match self.mode { - ViMode::Insert => Self::insert_single_key_event(&self.insert_keybindings, combo), + ViMode::Insert => self.default_key_event(&self.insert_keybindings, combo), ViMode::Normal | ViMode::Visual => self.normal_visual_single_key_event(combo), } } - fn insert_single_key_event(keybindings: &Keybindings, combo: KeyCombination) -> ReedlineEvent { - match combo.key_code { - KeyCode::Char(c) => keybindings - .find_binding(combo.modifier, KeyCode::Char(c)) - .unwrap_or_else(|| { - if combo.modifier == KeyModifiers::NONE - || combo.modifier == KeyModifiers::SHIFT - || combo.modifier == KeyModifiers::CONTROL | KeyModifiers::ALT - || combo.modifier - == KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT - { - ReedlineEvent::Edit(vec![EditCommand::InsertChar( - if combo.modifier == KeyModifiers::SHIFT { - c.to_ascii_uppercase() - } else { - c - }, - )]) - } else { - ReedlineEvent::None - } - }), - code => keybindings - .find_binding(combo.modifier, code) - .unwrap_or_else(|| { - if combo.modifier == KeyModifiers::NONE && code == KeyCode::Enter { - ReedlineEvent::Enter - } else { - ReedlineEvent::None - } - }), - } - } - fn normal_visual_single_key_event(&mut self, combo: KeyCombination) -> ReedlineEvent { let mode = self.mode; let keybindings = self.keybindings_for_mode(mode); @@ -295,30 +222,13 @@ impl Vi { }), } } - - fn with_flushed_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent { - let Some(flush_event) = self.flush_pending_sequence() else { - return event; - }; - - if matches!(event, ReedlineEvent::None) { - return flush_event; - } - - match flush_event { - ReedlineEvent::Multiple(mut events) => { - events.push(event); - ReedlineEvent::Multiple(events) - } - other => ReedlineEvent::Multiple(vec![other, event]), - } - } } #[cfg(test)] mod test { use super::*; - use crate::{EditCommand, KeyCombination}; + use crate::{enums::ReedlineRawEvent, EditCommand, KeyCombination}; + use crossterm::event::{Event, KeyEvent}; use pretty_assertions::assert_eq; #[test] From f63785145e2f0e4878a5ac17ea5da71cc59873ef Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 17:27:05 -0500 Subject: [PATCH 7/8] remove `ViExitToNormalMode` and replace with `ViChangeMode("normal")` --- src/edit_mode/vi/mod.rs | 11 ++++----- src/edit_mode/vi/vi_keybindings.rs | 2 +- src/engine.rs | 38 ++++++++++++------------------ src/enums.rs | 4 ---- src/lib.rs | 2 +- 5 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 44a86837..84623fb8 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -12,9 +12,7 @@ use self::motion::ViCharSearch; use super::EditMode; use crate::{ - edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, - enums::{EventStatus, ReedlineEvent}, - PromptEditMode, PromptViMode, + edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, enums::{EventStatus, ReedlineEvent}, PromptEditMode, PromptViMode }; #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -66,7 +64,7 @@ impl Vi { visual_keybindings.add_binding( KeyModifiers::NONE, KeyCode::Esc, - ReedlineEvent::ViExitToNormalMode, + ReedlineEvent::ViChangeMode("normal".into()), ); let _ = visual_keybindings.remove_binding(KeyModifiers::NONE, KeyCode::Char('v')); @@ -129,7 +127,6 @@ impl EditMode for Vi { ReedlineEvent::ViChangeMode(mode_str) => ViMode::from_str(&mode_str) .map(|mode| self.set_mode(mode)) .unwrap_or(EventStatus::Inapplicable), - ReedlineEvent::ViExitToNormalMode => self.set_mode(ViMode::Normal), _ => EventStatus::Inapplicable, } } @@ -239,7 +236,7 @@ mod test { .unwrap(); let result = vi.parse_event(esc); - assert_eq!(result, ReedlineEvent::ViExitToNormalMode); + assert_eq!(result, ReedlineEvent::ViChangeMode("normal".into())); } #[test] @@ -357,7 +354,7 @@ mod test { #[test] fn insert_sequence_binding_emits_event() { let mut insert_keybindings = default_vi_insert_keybindings(); - let exit_event = ReedlineEvent::ViExitToNormalMode; + let exit_event = ReedlineEvent::ViChangeMode("normal".into()); insert_keybindings.add_sequence_binding( vec![ KeyCombination { diff --git a/src/edit_mode/vi/vi_keybindings.rs b/src/edit_mode/vi/vi_keybindings.rs index 25988f79..46158cfc 100644 --- a/src/edit_mode/vi/vi_keybindings.rs +++ b/src/edit_mode/vi/vi_keybindings.rs @@ -52,7 +52,7 @@ pub fn default_vi_insert_keybindings() -> Keybindings { add_common_navigation_bindings(&mut kb); add_common_edit_bindings(&mut kb); add_common_selection_bindings(&mut kb); - kb.add_binding(KM::NONE, KC::Esc, ReedlineEvent::ViExitToNormalMode); + kb.add_binding(KM::NONE, KC::Esc, ReedlineEvent::ViChangeMode("normal".into())); kb } diff --git a/src/engine.rs b/src/engine.rs index da2c1b54..89052c76 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -989,13 +989,6 @@ impl Reedline { self.input_mode = InputMode::Regular; Ok(EventStatus::Handled) } - ReedlineEvent::ViExitToNormalMode => { - self.input_mode = InputMode::Regular; - let _ = self - .edit_mode - .handle_mode_specific_event(ReedlineEvent::ViExitToNormalMode); - Ok(EventStatus::Handled) - } // TODO: Check if events should be handled ReedlineEvent::Right | ReedlineEvent::Left @@ -1157,21 +1150,6 @@ impl Reedline { self.editor.clear_selection(); Ok(EventStatus::Handled) } - ReedlineEvent::ViExitToNormalMode => { - self.deactivate_menus(); - self.editor.clear_selection(); - let was_insert = matches!( - self.edit_mode.edit_mode(), - PromptEditMode::Vi(PromptViMode::Insert) - ); - let _ = self - .edit_mode - .handle_mode_specific_event(ReedlineEvent::ViExitToNormalMode); - if was_insert && self.editor.insertion_point() > 0 { - self.run_edit_commands(&[EditCommand::MoveLeft { select: false }]); - } - Ok(EventStatus::Handled) - } ReedlineEvent::CtrlD => { if self.editor.is_empty() { self.editor.reset_undo_stack(); @@ -1372,7 +1350,21 @@ impl Reedline { // Exhausting the event handlers is still considered handled Ok(EventStatus::Inapplicable) } - ReedlineEvent::ViChangeMode(_) => Ok(self.edit_mode.handle_mode_specific_event(event)), + ReedlineEvent::ViChangeMode(_) => { + let was_insert = matches!( + self.edit_mode.edit_mode(), + PromptEditMode::Vi(PromptViMode::Insert) + ); + let status = self.edit_mode.handle_mode_specific_event(event); + if matches!(status, EventStatus::Handled) { + self.editor.clear_selection(); + if was_insert && self.editor.insertion_point() > 0 { + self.run_edit_commands(&[EditCommand::MoveLeft { select: false }]); + } + } + + Ok(status) + } ReedlineEvent::None | ReedlineEvent::Mouse => Ok(EventStatus::Inapplicable), } } diff --git a/src/enums.rs b/src/enums.rs index 11bc4c56..3f1d9181 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -833,9 +833,6 @@ pub enum ReedlineEvent { /// Change mode (vi mode only) ViChangeMode(String), - - /// Exit vi insert/visual mode to normal - ViExitToNormalMode, } impl Display for ReedlineEvent { @@ -880,7 +877,6 @@ impl Display for ReedlineEvent { ReedlineEvent::ExecuteHostCommand(_) => write!(f, "ExecuteHostCommand"), ReedlineEvent::OpenEditor => write!(f, "OpenEditor"), ReedlineEvent::ViChangeMode(_) => write!(f, "ViChangeMode mode: "), - ReedlineEvent::ViExitToNormalMode => write!(f, "ViExitToNormalMode"), } } } diff --git a/src/lib.rs b/src/lib.rs index 985fdcbf..e46b149e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,7 @@ //! key_code: KeyCode::Char('j'), //! }, //! ], -//! ReedlineEvent::ViExitToNormalMode, +//! ReedlineEvent::ViChangeMode("normal".into()), //! ); //! //! let edit_mode = Box::new(Vi::new( From 2ddeaa0395236e721638c50e73b33f0b6de36ab4 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 28 Jan 2026 18:04:26 -0500 Subject: [PATCH 8/8] `cargo fmt --all` --- src/edit_mode/emacs.rs | 1 - src/edit_mode/vi/mod.rs | 4 +++- src/edit_mode/vi/vi_keybindings.rs | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/edit_mode/emacs.rs b/src/edit_mode/emacs.rs index 2a5dadab..75ff07ef 100644 --- a/src/edit_mode/emacs.rs +++ b/src/edit_mode/emacs.rs @@ -12,7 +12,6 @@ use crate::{ }; use crossterm::event::{KeyCode, KeyModifiers}; - /// Returns the current default emacs keybindings pub fn default_emacs_keybindings() -> Keybindings { use EditCommand as EC; diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 84623fb8..d08f65e0 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -12,7 +12,9 @@ use self::motion::ViCharSearch; use super::EditMode; use crate::{ - edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, enums::{EventStatus, ReedlineEvent}, PromptEditMode, PromptViMode + edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, + enums::{EventStatus, ReedlineEvent}, + PromptEditMode, PromptViMode, }; #[derive(Debug, PartialEq, Eq, Clone, Copy)] diff --git a/src/edit_mode/vi/vi_keybindings.rs b/src/edit_mode/vi/vi_keybindings.rs index 46158cfc..04e22b82 100644 --- a/src/edit_mode/vi/vi_keybindings.rs +++ b/src/edit_mode/vi/vi_keybindings.rs @@ -52,7 +52,11 @@ pub fn default_vi_insert_keybindings() -> Keybindings { add_common_navigation_bindings(&mut kb); add_common_edit_bindings(&mut kb); add_common_selection_bindings(&mut kb); - kb.add_binding(KM::NONE, KC::Esc, ReedlineEvent::ViChangeMode("normal".into())); + kb.add_binding( + KM::NONE, + KC::Esc, + ReedlineEvent::ViChangeMode("normal".into()), + ); kb }