diff --git a/examples/chords.rs b/examples/chords.rs new file mode 100644 index 00000000..4a525d01 --- /dev/null +++ b/examples/chords.rs @@ -0,0 +1,78 @@ +// Example demonstrating multi-keystroke chord support in reedline +// +// This example shows how to configure key chords (multi-key sequences) +// that trigger actions. For example, pressing Ctrl+X followed by Ctrl+C +// can be bound to a specific event. + +use crossterm::event::{KeyCode, KeyModifiers}; +use reedline::{ + default_emacs_keybindings, DefaultPrompt, Emacs, KeyCombination, Reedline, ReedlineEvent, + Signal, +}; +use std::io; + +/// Helper to create a KeyCombination for Ctrl+ +fn ctrl(c: char) -> KeyCombination { + KeyCombination { + modifier: KeyModifiers::CONTROL, + key_code: KeyCode::Char(c), + } +} + +fn main() -> io::Result<()> { + println!("Reedline Chord Example"); + println!("======================"); + println!(); + println!("This example demonstrates multi-keystroke chord bindings."); + println!(); + println!("Available chords:"); + println!(" Ctrl+X Ctrl+C - Quit"); + println!(" Ctrl+X Ctrl+Y Ctrl+Z Ctrl+Z Ctrl+Y - Report that nothing happens"); + println!(); + println!("Regular keys and single-key bindings still work normally."); + println!("You may also type 'exit' or press Ctrl+D to quit."); + println!(); + + // Start with the default Emacs keybindings + let mut keybindings = default_emacs_keybindings(); + + // Add chord bindings + // Ctrl+X Ctrl+C: Quit + keybindings.add_sequence_binding(&[ctrl('x'), ctrl('c')], ReedlineEvent::CtrlD); + + // Ctrl+X Ctrl+Y Ctrl+Z Ctrl+Z Ctrl+Y: Quit + keybindings.add_sequence_binding( + &[ctrl('x'), ctrl('y'), ctrl('z'), ctrl('z'), ctrl('y')], + ReedlineEvent::ExecuteHostCommand(String::from("Nothing happens")), + ); + + // Create the Emacs edit mode with our custom keybindings + let edit_mode = Box::new(Emacs::new(keybindings)); + + // Create the line editor + let mut line_editor = Reedline::create().with_edit_mode(edit_mode); + + let prompt = DefaultPrompt::default(); + + loop { + let sig = line_editor.read_line(&prompt)?; + match sig { + Signal::Success(buffer) => { + if buffer == "exit" { + println!("Goodbye!"); + break; + } + println!("You typed: {buffer}"); + } + Signal::CtrlD => { + println!("\nQuitting."); + break; + } + Signal::CtrlC => { + println!("Ctrl+C pressed"); + } + } + } + + Ok(()) +} diff --git a/src/edit_mode/emacs.rs b/src/edit_mode/emacs.rs index 155a0741..75de2a80 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, KeyBindingTarget, KeyCombination, + Keybindings, }, EditMode, }, @@ -104,12 +105,16 @@ pub fn default_emacs_keybindings() -> Keybindings { /// This parses the incoming Events like a emacs style-editor pub struct Emacs { + /// Cache for multi-key sequences (chords); will be empty when not in a known chord + cache: Vec, + /// Keybindings for this mode keybindings: Keybindings, } impl Default for Emacs { fn default() -> Self { Emacs { + cache: Vec::new(), keybindings: default_emacs_keybindings(), } } @@ -120,50 +125,10 @@ 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), - }, - + }) => self.parse_key_event(KeyCombination { + key_code: code, + modifier: modifiers, + }), Event::Mouse(_) => ReedlineEvent::Mouse, Event::Resize(width, height) => ReedlineEvent::Resize(width, height), Event::FocusGained => ReedlineEvent::None, @@ -182,7 +147,72 @@ impl EditMode for Emacs { 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, + cache: Vec::new(), + } + } + + fn parse_key_event(&mut self, combo: KeyCombination) -> ReedlineEvent { + self.cache.push(normalize_key_combo(&combo)); + + match self.keybindings.find_sequence_binding(&self.cache) { + Some(KeyBindingTarget::Event(event)) => { + // Found a complete binding, clear the cache and return the event + self.cache.clear(); + event + } + Some(KeyBindingTarget::ChordPrefix) => { + // Partial match, wait for the next key + ReedlineEvent::None + } + None => { + // No match, clear the cache. + self.cache.clear(); + + // Check fallback condition of just inserting a normal character. + match combo.key_code { + KeyCode::Char(c) => { + 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(c)]) + } else { + ReedlineEvent::None + } + } + _ => ReedlineEvent::None, + } + } + } + } +} + +fn normalize_key_combo(combo: &KeyCombination) -> KeyCombination { + match combo.key_code { + 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 code = match combo.modifier { + KeyModifiers::NONE => combo.key_code, + _ => KeyCode::Char(c.to_ascii_lowercase()), + }; + + KeyCombination { + modifier: combo.modifier, + key_code: code, + } + } + _ => combo.clone(), } } diff --git a/src/edit_mode/keybindings.rs b/src/edit_mode/keybindings.rs index ef4636c7..b236d215 100644 --- a/src/edit_mode/keybindings.rs +++ b/src/edit_mode/keybindings.rs @@ -5,17 +5,49 @@ use { std::collections::HashMap, }; +/// Representation of a key combination: modifier + key code #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Debug)] pub struct KeyCombination { + /// Modifier keys pub modifier: KeyModifiers, + /// The key code pub key_code: KeyCode, } /// Main definition of editor keybindings -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Clone, Debug)] pub struct Keybindings { - /// Defines a keybinding for a reedline event - pub bindings: HashMap, + /// Trie mapping key combination sequences to their corresponding events. + root: KeyBindingNode, +} + +/// Target that a key combination may be bound to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum KeyBindingTarget { + /// Indicates a binding to an event. + Event(ReedlineEvent), + /// Indicates that this is a prefix to other bindings. + ChordPrefix, +} + +// TODO: Implement Serialize for Keybindings +// TODO: Implement Deserialize for Keybindings + +/// Trie node that represents a key combination's binding. The key +/// combination may *only* be bound to an event, or may be a +/// strict prefix of a chord of key combinations. +#[derive(Clone, Debug)] +enum KeyBindingNode { + /// Indicates a binding to an event. + Event(ReedlineEvent), + /// Indicates that this is a prefix to other bindings. + Prefix(HashMap), +} + +impl KeyBindingNode { + fn new_prefix() -> Self { + KeyBindingNode::Prefix(HashMap::new()) + } } impl Default for Keybindings { @@ -25,14 +57,14 @@ impl Default for Keybindings { } impl Keybindings { - /// New keybining + /// Returns a new, empty keybinding set pub fn new() -> Self { Self { - bindings: HashMap::new(), + root: KeyBindingNode::new_prefix(), } } - /// Defines an empty keybinding object + /// Returns a new, empty keybinding set pub fn empty() -> Self { Self::new() } @@ -56,16 +88,95 @@ impl Keybindings { } let key_combo = KeyCombination { modifier, key_code }; - self.bindings.insert(key_combo, command); + + self.add_sequence_binding(&[key_combo], command); + } + + /// Binds a sequence of key combinations to an event. This allows binding a single + /// key combination, as well as binding a chord of multiple key combinations. + pub fn add_sequence_binding(&mut self, sequence: &[KeyCombination], event: ReedlineEvent) { + if sequence.is_empty() { + return; + } + + let mut current_target = &mut self.root; + + for combo in sequence.iter().take(sequence.len() - 1) { + match current_target { + KeyBindingNode::Prefix(ref mut map) => { + current_target = map + .entry(combo.clone()) + .or_insert_with(KeyBindingNode::new_prefix); + } + KeyBindingNode::Event(_) => { + // Overwrite existing event binding with a prefix + *current_target = KeyBindingNode::new_prefix(); + } + } + } + + let final_combo = &sequence[sequence.len() - 1]; + + match current_target { + KeyBindingNode::Prefix(ref mut map) => { + map.insert(final_combo.clone(), KeyBindingNode::Event(event)); + } + KeyBindingNode::Event(_) => { + // Overwrite existing event binding with a prefix initialized with the event. + let prefix = KeyBindingNode::Prefix(HashMap::from([( + final_combo.clone(), + KeyBindingNode::Event(event), + )])); + *current_target = prefix; + } + } } - /// Find a keybinding based on the modifier and keycode + /// 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() + self.find_sequence_binding(&[key_combo]).and_then(|target| { + if let KeyBindingTarget::Event(event) = target { + Some(event) + } else { + None + } + }) } - /// Remove a keybinding + /// Find a keybinding based on a sequence of key combinations. + /// + /// Returns `Some(KeyBindingTarget::Event(ReedlineEvent))` if the sequence is bound to a + /// particular [`ReedlineEvent`]. + /// + /// Returns `Some(KeyBindingTarget::ChordPrefix)` if the sequence is a strict prefix + /// of other bindings. + /// + /// Returns `None` if the sequence is not bound. + pub fn find_sequence_binding(&self, sequence: &[KeyCombination]) -> Option { + let mut current_target = &self.root; + + for combo in sequence { + match current_target { + KeyBindingNode::Prefix(map) => { + if let Some(next_target) = map.get(combo) { + current_target = next_target; + } else { + return None; + } + } + KeyBindingNode::Event(_) => return None, + } + } + + match current_target { + KeyBindingNode::Prefix(_) => Some(KeyBindingTarget::ChordPrefix), + KeyBindingNode::Event(event) => Some(KeyBindingTarget::Event(event.clone())), + } + } + + /// Remove a single-key keybinding. If the indicated key combination is a strict prefix + /// of chord bindings, those latter bindings are preserved. /// /// Returns `Some(ReedlineEvent)` if the key combination was previously bound to a particular [`ReedlineEvent`] pub fn remove_binding( @@ -74,12 +185,72 @@ impl Keybindings { key_code: KeyCode, ) -> Option { let key_combo = KeyCombination { modifier, key_code }; - self.bindings.remove(&key_combo) + self.remove_sequence_binding(&[key_combo]) + } + + /// Unbind a sequence of key combinations. If the given sequence is a strict prefix + /// of other bindings, those bindings are preserved. + /// + /// Returns `Some(ReedlineEvent)` if the sequence was previously bound to a particular [`ReedlineEvent`] + pub fn remove_sequence_binding( + &mut self, + sequence: &[KeyCombination], + ) -> Option { + let mut current_target = &mut self.root; + + if sequence.is_empty() { + return None; + } + + for combo in sequence.iter().take(sequence.len() - 1) { + match current_target { + KeyBindingNode::Prefix(map) => { + if let Some(next_target) = map.get_mut(combo) { + current_target = next_target; + } else { + return None; + } + } + KeyBindingNode::Event(_) => return None, + } + } + + let final_combo = &sequence[sequence.len() - 1]; + + match current_target { + KeyBindingNode::Prefix(map) => { + // Make sure it's a terminal node before we try to remove it. + if !matches!(map.get(final_combo), Some(KeyBindingNode::Event(_))) { + None + } else if let Some(KeyBindingNode::Event(old_event)) = map.remove(final_combo) { + Some(old_event) + } else { + None + } + } + KeyBindingNode::Event(_) => None, + } } - /// Get assigned keybindings - pub fn get_keybindings(&self) -> &HashMap { - &self.bindings + /// Get assigned single-key keybindings. + pub fn get_keybindings(&self) -> impl IntoIterator { + self.get_sequence_bindings() + .into_iter() + .filter_map(|(seq, event)| { + if let [first, ..] = seq { + Some((first, event)) + } else { + None + } + }) + } + + /// Get all bindings for key sequences, including single-key bindings and chords. + pub fn get_sequence_bindings( + &self, + ) -> impl IntoIterator { + // TODO + [] } } @@ -288,3 +459,46 @@ pub fn add_common_selection_bindings(kb: &mut Keybindings) { edit_bind(EC::SelectAll), ); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_chord_lookup() { + const BOUND_EVENT: ReedlineEvent = ReedlineEvent::MenuDown; + + let mut kb = Keybindings::new(); + + let sequence = vec![ + KeyCombination { + modifier: KeyModifiers::CONTROL, + key_code: KeyCode::Char('x'), + }, + KeyCombination { + modifier: KeyModifiers::CONTROL, + key_code: KeyCode::Char('c'), + }, + ]; + + kb.add_sequence_binding(&sequence, BOUND_EVENT); + + // Make sure we can find the prefix. + let found_prefix = kb.find_sequence_binding(&sequence[0..1]); + assert_eq!(found_prefix, Some(KeyBindingTarget::ChordPrefix)); + + // Make sure we can find the binding. + let found_binding = kb.find_sequence_binding(&sequence); + assert_eq!(found_binding, Some(KeyBindingTarget::Event(BOUND_EVENT))); + + // Make sure we can't find some non-existent binding. + let not_found = kb.find_sequence_binding(&[ + sequence[0].clone(), + KeyCombination { + modifier: KeyModifiers::CONTROL, + key_code: KeyCode::Char('z'), + }, + ]); + assert_eq!(not_found, None); + } +} diff --git a/src/edit_mode/mod.rs b/src/edit_mode/mod.rs index 38e1456f..ce90e2d2 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, Keybindings}; pub use vi::{default_vi_insert_keybindings, default_vi_normal_keybindings, Vi}; diff --git a/src/lib.rs b/src/lib.rs index d202a220..f7032b30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -262,7 +262,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, Keybindings, Vi, }; mod highlighter; diff --git a/src/utils/query.rs b/src/utils/query.rs index 2b762e9a..1fd5785e 100644 --- a/src/utils/query.rs +++ b/src/utils/query.rs @@ -133,7 +133,7 @@ fn get_keybinding_strings( ) -> Vec<(String, String, String, String)> { let mut data: Vec<(String, String, String, String)> = keybindings .get_keybindings() - .iter() + .into_iter() .map(|(combination, event)| { ( mode.to_string(),