//! Modal keymap resolution (MJB-HLR-015). //! //! Replaces the application template's resolver, which looked up single keys //! first and otherwise accumulated a buffer cleared on every `Action::Tick` — //! giving multi-key sequences a ~250 ms timeout at the default tick rate, so //! `gg` failed if typed slowly. Here the set of proper prefixes is precomputed, //! so resolution is exact and **time-independent** (MJB-LLR-154). //! //! A static table cannot express everything a modal editor needs: insert mode //! must treat any unbound printable key as self-insert, and counts are typed as //! ordinary digits. Both are handled around the table rather than in it, by //! [`KeymapResult::Cancelled`] and by count accumulation. use std::collections::{HashMap, HashSet}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use super::command::Command; use crate::config::{KeyBindings, Mode}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum KeymapResult { /// The keys so far are a prefix of at least one binding; wait for more. Pending, /// A binding matched, carrying any count typed before it. Matched(Command, Option), /// The keys match nothing. Carries what was accumulated so the caller can /// apply a mode-specific fallback, such as insert-mode self-insert. Cancelled(Vec), } #[derive(Debug, Default)] pub struct Keymap { bindings: HashMap, Command>>, /// MJB-LLR-150: every proper prefix of every bound sequence, per mode. prefixes: HashMap>>, pending: Vec, count: Option, } impl Keymap { /// MJB-LLR-150 pub fn new(bindings: &KeyBindings) -> Self { let mut prefixes: HashMap>> = HashMap::new(); for (mode, map) in &bindings.0 { let set = prefixes.entry(*mode).or_default(); for keys in map.keys() { for n in 1..keys.len() { set.insert(keys[..n].to_vec()); } } } Self { bindings: bindings.0.clone(), prefixes, pending: Vec::new(), count: None, } } pub fn pending(&self) -> &[KeyEvent] { &self.pending } pub fn count(&self) -> Option { self.count } /// Abandon any partial sequence and count, e.g. on a mode change. pub fn reset(&mut self) { self.pending.clear(); self.count = None; } /// Look up a single key without disturbing pending state. Used by `App` for /// the `Global` map, which has no multi-key bindings (MJB-LLR-203). pub fn lookup_single(&self, mode: Mode, key: KeyEvent) -> Option { self.bindings.get(&mode)?.get(&vec![key]).copied() } /// MJB-LLR-151..156: feed one key and resolve. pub fn resolve(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult { // MJB-LLR-155: digits typed before a command form a count. Only while // no sequence is pending, so `g` then `1` is not swallowed. if self.pending.is_empty() && matches!(mode, Mode::Normal | Mode::Select) && let KeyCode::Char(c) = key.code && c.is_ascii_digit() && !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) { let digit = (c as u8 - b'0') as usize; // A leading zero is not a count; it stays available as a binding. if digit != 0 || self.count.is_some() { self.count = Some(self.count.unwrap_or(0) * 10 + digit); return KeymapResult::Pending; } } self.pending.push(key); // MJB-LLR-151 if let Some(&cmd) = self.bindings.get(&mode).and_then(|m| m.get(&self.pending)) { let count = self.count.take(); self.pending.clear(); return KeymapResult::Matched(cmd, count); } // MJB-LLR-152 if self .prefixes .get(&mode) .is_some_and(|set| set.contains(&self.pending)) { return KeymapResult::Pending; } // MJB-LLR-153 let keys = std::mem::take(&mut self.pending); self.count = None; KeymapResult::Cancelled(keys) } } /// MJB-LLR-156: the insert-mode fallback — a bare printable character. /// /// Control and Alt are excluded so an unbound `Ctrl-x` is discarded rather than /// inserting `x`. Shift is allowed: it is how capitals are typed. pub fn self_insert_char(keys: &[KeyEvent]) -> Option { let [key] = keys else { return None; }; let KeyCode::Char(c) = key.code else { return None; }; if key.modifiers.contains(KeyModifiers::CONTROL) || key.modifiers.contains(KeyModifiers::ALT) { return None; } Some(c) } #[cfg(test)] mod tests { use super::*; use crate::config::parse_key_sequence; fn key(c: char) -> KeyEvent { KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()) } fn ctrl(c: char) -> KeyEvent { KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL) } fn keymap() -> Keymap { let mut bindings = KeyBindings::default(); let mut normal = HashMap::new(); normal.insert(parse_key_sequence("").unwrap(), Command::MoveCharLeft); normal.insert(parse_key_sequence("").unwrap(), Command::GotoFileStart); normal.insert(parse_key_sequence("").unwrap(), Command::GotoLastLine); bindings.0.insert(Mode::Normal, normal); let mut insert = HashMap::new(); insert.insert(parse_key_sequence("").unwrap(), Command::NormalMode); bindings.0.insert(Mode::Insert, insert); Keymap::new(&bindings) } /// MJB-LLR-150: every proper prefix is precomputed, and only proper /// prefixes — a complete binding is not itself registered as a prefix, or /// it would never resolve. #[test] fn mjb_llr_150_proper_prefixes_are_precomputed() { let k = keymap(); let set = k.prefixes.get(&Mode::Normal).expect("normal prefixes"); assert!( set.contains(&parse_key_sequence("").unwrap()), "`g` is a proper prefix of `gg` and `ge`" ); assert!( !set.contains(&parse_key_sequence("").unwrap()), "a complete binding must not be registered as a prefix" ); assert!( !set.contains(&parse_key_sequence("").unwrap()), "a single-key binding has no proper prefix" ); } #[test] fn mjb_llr_150_prefix_set_is_empty_for_a_mode_without_sequences() { let k = keymap(); let set = k.prefixes.get(&Mode::Insert).expect("insert prefixes"); assert!(set.is_empty(), "insert has only single-key bindings"); } #[test] fn mjb_llr_151_single_key_binding_matches_immediately() { let mut k = keymap(); assert_eq!( k.resolve(Mode::Normal, key('h')), KeymapResult::Matched(Command::MoveCharLeft, None) ); assert!(k.pending().is_empty(), "pending must be cleared"); } /// MJB-LLR-152, MJB-LLR-154: `g` is a prefix, so it waits — indefinitely, /// with no timer involved. This is the bug the old resolver had. #[test] fn mjb_llr_152_prefix_key_waits_for_more() { let mut k = keymap(); assert_eq!(k.resolve(Mode::Normal, key('g')), KeymapResult::Pending); assert_eq!(k.pending().len(), 1); } #[test] fn mjb_llr_151_two_key_sequence_resolves() { let mut k = keymap(); assert_eq!(k.resolve(Mode::Normal, key('g')), KeymapResult::Pending); assert_eq!( k.resolve(Mode::Normal, key('g')), KeymapResult::Matched(Command::GotoFileStart, None) ); } #[test] fn mjb_llr_151_sequences_sharing_a_prefix_stay_distinct() { let mut k = keymap(); k.resolve(Mode::Normal, key('g')); assert_eq!( k.resolve(Mode::Normal, key('e')), KeymapResult::Matched(Command::GotoLastLine, None) ); } #[test] fn mjb_llr_153_unknown_continuation_cancels() { let mut k = keymap(); k.resolve(Mode::Normal, key('g')); let got = k.resolve(Mode::Normal, key('z')); match got { KeymapResult::Cancelled(keys) => assert_eq!(keys, vec![key('g'), key('z')]), other => panic!("expected Cancelled, got {other:?}"), } assert!(k.pending().is_empty()); } #[test] fn mjb_llr_153_unbound_key_cancels_immediately() { let mut k = keymap(); match k.resolve(Mode::Normal, key('z')) { KeymapResult::Cancelled(keys) => assert_eq!(keys, vec![key('z')]), other => panic!("expected Cancelled, got {other:?}"), } } /// MJB-LLR-154: no elapsed-time input exists, so a pending sequence /// survives arbitrarily many unrelated resolutions in other modes. #[test] fn mjb_llr_154_pending_is_not_discarded_by_time() { let mut k = keymap(); k.resolve(Mode::Normal, key('g')); assert_eq!(k.pending().len(), 1); // Nothing but another key can advance or clear it. assert_eq!( k.resolve(Mode::Normal, key('g')), KeymapResult::Matched(Command::GotoFileStart, None) ); } #[test] fn mjb_llr_155_count_accumulates_and_is_delivered() { let mut k = keymap(); assert_eq!(k.resolve(Mode::Normal, key('1')), KeymapResult::Pending); assert_eq!(k.resolve(Mode::Normal, key('2')), KeymapResult::Pending); assert_eq!( k.resolve(Mode::Normal, key('h')), KeymapResult::Matched(Command::MoveCharLeft, Some(12)) ); } #[test] fn mjb_llr_155_count_is_consumed_once() { let mut k = keymap(); k.resolve(Mode::Normal, key('3')); k.resolve(Mode::Normal, key('h')); assert_eq!( k.resolve(Mode::Normal, key('h')), KeymapResult::Matched(Command::MoveCharLeft, None), "the count must not persist to the next command" ); } #[test] fn mjb_llr_155_leading_zero_is_not_a_count() { let mut k = keymap(); // `0` with no count in progress falls through to binding lookup. match k.resolve(Mode::Normal, key('0')) { KeymapResult::Cancelled(_) => {} other => panic!("expected Cancelled for unbound 0, got {other:?}"), } assert_eq!(k.count(), None); } #[test] fn mjb_llr_155_zero_extends_an_existing_count() { let mut k = keymap(); k.resolve(Mode::Normal, key('1')); k.resolve(Mode::Normal, key('0')); assert_eq!( k.resolve(Mode::Normal, key('h')), KeymapResult::Matched(Command::MoveCharLeft, Some(10)) ); } #[test] fn mjb_llr_155_digits_are_not_counts_in_insert_mode() { let mut k = keymap(); match k.resolve(Mode::Insert, key('5')) { KeymapResult::Cancelled(keys) => { assert_eq!(self_insert_char(&keys), Some('5'), "must type a 5"); } other => panic!("expected Cancelled, got {other:?}"), } } #[test] fn mjb_llr_156_self_insert_accepts_plain_printables() { assert_eq!(self_insert_char(&[key('a')]), Some('a')); assert_eq!( self_insert_char(&[KeyEvent::new( KeyCode::Char('A'), KeyModifiers::SHIFT )]), Some('A'), "shift is how capitals are typed" ); } #[test] fn mjb_llr_156_self_insert_rejects_control_and_alt() { assert_eq!(self_insert_char(&[ctrl('x')]), None); assert_eq!( self_insert_char(&[KeyEvent::new(KeyCode::Char('x'), KeyModifiers::ALT)]), None ); } #[test] fn mjb_llr_156_self_insert_rejects_non_char_and_sequences() { assert_eq!( self_insert_char(&[KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())]), None ); assert_eq!( self_insert_char(&[key('a'), key('b')]), None, "only a single key can self-insert" ); } #[test] fn mjb_llr_203_lookup_single_does_not_disturb_pending() { let mut k = keymap(); k.resolve(Mode::Normal, key('g')); assert_eq!(k.lookup_single(Mode::Normal, key('h')), Some(Command::MoveCharLeft)); assert_eq!(k.pending().len(), 1, "global lookup must be side-effect free"); } #[test] fn reset_clears_pending_and_count() { let mut k = keymap(); k.resolve(Mode::Normal, key('3')); k.resolve(Mode::Normal, key('g')); k.reset(); assert!(k.pending().is_empty()); assert_eq!(k.count(), None); } #[test] fn unknown_mode_cancels_rather_than_panicking() { let mut k = keymap(); match k.resolve(Mode::Command, key('x')) { KeymapResult::Cancelled(_) => {} other => panic!("expected Cancelled for an unmapped mode, got {other:?}"), } } }