diff --git a/README.md b/README.md index c2dc7455..f0fe7879 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,41 @@ 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::ViExitToNormalMode, +); + +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/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/base.rs b/src/edit_mode/base.rs index 408098b3..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; @@ -18,4 +70,53 @@ pub trait EditMode: Send { fn handle_mode_specific_event(&mut self, _event: ReedlineEvent) -> EventStatus { 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 + } + + /// 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..75ff07ef 100644 --- a/src/edit_mode/emacs.rs +++ b/src/edit_mode/emacs.rs @@ -2,14 +2,15 @@ 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, }, - 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 { @@ -105,90 +106,58 @@ 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(), } } } impl EditMode for Emacs { - fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent { - 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"), - )]), - } + 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 { PromptEditMode::Emacs } + + fn has_pending_sequence(&self) -> bool { + self.sequence_state.is_pending() + } + + fn flush_pending_sequence(&mut self) -> Option { + let keybindings = &self.keybindings; + let resolution = self.sequence_state.flush_with_combos(); + resolution.into_event(|combo| self.default_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(), + } } } #[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 ef4636c7..a815d71f 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -5,17 +5,100 @@ 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, } +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), + Prefix, + ExactAndPrefix(ReedlineEvent), + NoMatch, +} + +/// State used to track partial key sequence matches. +#[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)>, +} + +/// 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, +} + +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 #[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 + pub sequence_bindings: HashMap, } impl Default for Keybindings { @@ -29,14 +112,10 @@ impl Keybindings { pub fn new() -> Self { Self { bindings: HashMap::new(), + sequence_bindings: HashMap::new(), } } - /// Defines an empty keybinding object - pub fn empty() -> Self { - Self::new() - } - /// Adds a keybinding /// /// # Panics @@ -59,12 +138,58 @@ 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::from(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::from(sequence.to_vec())) + .cloned(); + + let is_prefix = self.sequence_bindings.keys().any(|key_sequence| { + let keys = key_sequence.as_ref(); + keys.len() > sequence.len() && keys[..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 +202,148 @@ 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::from(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, + ) -> 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(); + + while let StepOutcome::Continue = self.process_step(keybindings, &mut resolution) {} + + if self.buffer.is_empty() { + self.pending_exact = None; + } + resolution + } + + pub fn flush_with_combos(&mut self) -> SequenceResolution { + if self.buffer.is_empty() { + return SequenceResolution::default(); + } + + let mut resolution = SequenceResolution::default(); + + if !self.flush_pending_exact(&mut resolution) { + resolution.combos = std::mem::take(&mut self.buffer); + } + + self.pending_exact = None; + resolution + } + + fn flush_pending_exact(&mut self, resolution: &mut SequenceResolution) -> bool { + let Some((pending_len, pending_event)) = self.pending_exact.take() else { + return false; + }; + + let pending_len = pending_len.min(self.buffer.len()); + self.buffer.drain(..pending_len); + resolution.events.push(pending_event); + true + } + + 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 + } + 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; + } + + // 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 { @@ -288,3 +551,104 @@ pub fn add_common_selection_bindings(kb: &mut Keybindings) { edit_bind(EC::SelectAll), ); } + +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::*; + 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')); + assert_eq!(first.into_event(fallback), None); + + let second = state.process_combo(&keybindings, combo('j')); + assert_eq!(second.into_event(fallback), 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')); + let second = state.process_combo(&keybindings, combo('k')); + + assert_eq!( + second.into_event(fallback), + 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')); + let flushed = state.flush_with_combos(); + + assert_eq!( + flushed.into_event(fallback), + 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..d08f65e0 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -5,15 +5,15 @@ 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; use super::EditMode; use crate::{ - edit_mode::{keybindings::Keybindings, vi::parser::parse}, - enums::{EditCommand, EventStatus, ReedlineEvent, ReedlineRawEvent}, + edit_mode::{keybindings::Keybindings, vi::parser::parse, KeyCombination, KeySequenceState}, + enums::{EventStatus, ReedlineEvent}, PromptEditMode, PromptViMode, }; @@ -42,6 +42,8 @@ pub struct Vi { cache: Vec, insert_keybindings: Keybindings, normal_keybindings: Keybindings, + visual_keybindings: Keybindings, + sequence_state: KeySequenceState, mode: ViMode, previous: Option, // last f, F, t, T motion for ; and , @@ -50,148 +52,69 @@ 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(), - 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::ViChangeMode("normal".into()), + ); + 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, } } } impl EditMode for Vi { - fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent { - 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)) => { - // 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 - } - }) - } - (_, KeyModifiers::NONE, KeyCode::Esc) => { - self.cache.clear(); - self.mode = ViMode::Normal; - 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 - .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 - } - }), - }, - - 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"), - )]), + 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 { @@ -203,36 +126,133 @@ 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; - EventStatus::Handled - } - Err(_) => EventStatus::Inapplicable, - }, + ReedlineEvent::ViChangeMode(mode_str) => ViMode::from_str(&mode_str) + .map(|mode| self.set_mode(mode)) + .unwrap_or(EventStatus::Inapplicable), _ => EventStatus::Inapplicable, } } + + fn has_pending_sequence(&self) -> bool { + self.sequence_state.is_pending() + } + + fn flush_pending_sequence(&mut self) -> Option { + let resolution = self.sequence_state.flush_with_combos(); + resolution.into_event(|combo| self.single_key_event_without_sequences(combo)) + } +} + +impl Vi { + fn set_mode(&mut self, mode: ViMode) -> EventStatus { + self.mode = mode; + self.cache.clear(); + self.sequence_state.clear(); + EventStatus::Handled + } + + 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 single_key_event_without_sequences(&mut self, combo: KeyCombination) -> ReedlineEvent { + match self.mode { + ViMode::Insert => self.default_key_event(&self.insert_keybindings, combo), + ViMode::Normal | ViMode::Visual => self.normal_visual_single_key_event(combo), + } + } + + 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 !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 { + 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 + } + }), + } + } } #[cfg(test)] mod test { use super::*; + use crate::{enums::ReedlineRawEvent, EditCommand, KeyCombination}; + use crossterm::event::{Event, KeyEvent}; 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::ViChangeMode("normal".into())); + } + + #[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] @@ -332,4 +352,172 @@ 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::ViChangeMode("normal".into()); + 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); + } + + #[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, + }, + ])]) + ); + } } diff --git a/src/edit_mode/vi/vi_keybindings.rs b/src/edit_mode/vi/vi_keybindings.rs index 20cca81a..04e22b82 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,18 @@ 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::ViChangeMode("normal".into()), + ); kb } diff --git a/src/engine.rs b/src/engine.rs index bdb04373..89052c76 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, @@ -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); } @@ -1289,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/lib.rs b/src/lib.rs index d202a220..e46b149e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,41 @@ //! 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::ViChangeMode("normal".into()), +//! ); +//! +//! 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 +297,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;