diff --git a/src/command/keymap.rs b/src/command/keymap.rs index cfbf0e2..4ec8811 100644 --- a/src/command/keymap.rs +++ b/src/command/keymap.rs @@ -100,9 +100,12 @@ pub enum Binding { } /// A keymap maps key patterns to bindings (command names or prefix sub-keymaps). +/// Supports Emacs-style parent keymap inheritance: if a key is not found in this +/// keymap, lookup falls through to the parent. #[derive(Default)] pub struct Keymap { bindings: HashMap, + parent: Option>, } impl fmt::Debug for Keymap { @@ -117,6 +120,16 @@ impl Keymap { pub fn new() -> Self { Self { bindings: HashMap::new(), + parent: None, + } + } + + /// Create a keymap that inherits from a parent. Keys not found here + /// fall through to the parent (Emacs-style keymap inheritance). + pub fn with_parent(parent: Arc) -> Self { + Self { + bindings: HashMap::new(), + parent: Some(parent), } } @@ -198,11 +211,8 @@ impl Keymap { hints } - /// Look up the binding for a key. - /// For Char keys, if exact (key, mods) match fails, retries with NONE - /// modifiers since terminals vary in whether they send SHIFT for - /// uppercase/symbol characters. - pub fn lookup(&self, key: KeyCode, mods: KeyModifiers) -> Option<&Binding> { + /// Look up the binding for a key in this keymap's own bindings. + fn lookup_local(&self, key: KeyCode, mods: KeyModifiers) -> Option<&Binding> { self.bindings .get(&KeyPattern::Key(key, mods)) .or_else(|| { @@ -223,6 +233,13 @@ impl Keymap { .or_else(|| self.bindings.get(&KeyPattern::Any)) } + /// Look up the binding for a key, falling through to parent keymaps. + pub fn lookup(&self, key: KeyCode, mods: KeyModifiers) -> Option<&Binding> { + self.lookup_local(key, mods).or_else(|| { + self.parent.as_ref().and_then(|p| p.lookup(key, mods)) + }) + } + /// Dispatch a key: look up binding, resolve through registry, return effects. pub fn dispatch( &self, @@ -712,36 +729,50 @@ impl KeymapSet { // ── Editing mode ───────────────────────────────────────────────── let mut ed = Keymap::new(); - ed.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); - ed.bind(KeyCode::Enter, none, "commit-cell-edit"); - ed.bind(KeyCode::Tab, none, "commit-and-advance-right"); + ed.bind_seq(KeyCode::Esc, none, vec![ + ("clear-buffer", vec!["edit".into()]), + ("enter-mode", vec!["normal".into()]), + ]); + ed.bind_seq(KeyCode::Enter, none, vec![ + ("commit-cell-edit", vec![]), + ("clear-buffer", vec!["edit".into()]), + ]); + ed.bind_seq(KeyCode::Tab, none, vec![ + ("commit-and-advance-right", vec![]), + ("clear-buffer", vec!["edit".into()]), + ]); ed.bind_args(KeyCode::Backspace, none, "pop-char", vec!["edit".into()]); ed.bind_any_char("append-char", vec!["edit".into()]); set.insert(ModeKey::Editing, Arc::new(ed)); // ── Formula edit ───────────────────────────────────────────────── let mut fe = Keymap::new(); - fe.bind_args( - KeyCode::Esc, - none, - "enter-mode", - vec!["formula-panel".into()], - ); - fe.bind(KeyCode::Enter, none, "commit-formula"); + fe.bind_seq(KeyCode::Esc, none, vec![ + ("clear-buffer", vec!["formula".into()]), + ("enter-mode", vec!["formula-panel".into()]), + ]); + fe.bind_seq(KeyCode::Enter, none, vec![ + ("commit-formula", vec![]), + ("clear-buffer", vec!["formula".into()]), + ]); fe.bind_args(KeyCode::Backspace, none, "pop-char", vec!["formula".into()]); fe.bind_any_char("append-char", vec!["formula".into()]); set.insert(ModeKey::FormulaEdit, Arc::new(fe)); // ── Category add ───────────────────────────────────────────────── let mut ca = Keymap::new(); - ca.bind_args( - KeyCode::Esc, - none, - "enter-mode", - vec!["category-panel".into()], - ); - ca.bind(KeyCode::Enter, none, "commit-category-add"); - ca.bind(KeyCode::Tab, none, "commit-category-add"); + ca.bind_seq(KeyCode::Esc, none, vec![ + ("clear-buffer", vec!["category".into()]), + ("enter-mode", vec!["category-panel".into()]), + ]); + ca.bind_seq(KeyCode::Enter, none, vec![ + ("commit-category-add", vec![]), + ("clear-buffer", vec!["category".into()]), + ]); + ca.bind_seq(KeyCode::Tab, none, vec![ + ("commit-category-add", vec![]), + ("clear-buffer", vec!["category".into()]), + ]); ca.bind_args( KeyCode::Backspace, none, @@ -753,30 +784,46 @@ impl KeymapSet { // ── Item add ───────────────────────────────────────────────────── let mut ia = Keymap::new(); - ia.bind_args( - KeyCode::Esc, - none, - "enter-mode", - vec!["category-panel".into()], - ); - ia.bind(KeyCode::Enter, none, "commit-item-add"); - ia.bind(KeyCode::Tab, none, "commit-item-add"); + ia.bind_seq(KeyCode::Esc, none, vec![ + ("clear-buffer", vec!["item".into()]), + ("enter-mode", vec!["category-panel".into()]), + ]); + ia.bind_seq(KeyCode::Enter, none, vec![ + ("commit-item-add", vec![]), + ("clear-buffer", vec!["item".into()]), + ]); + ia.bind_seq(KeyCode::Tab, none, vec![ + ("commit-item-add", vec![]), + ("clear-buffer", vec!["item".into()]), + ]); ia.bind_args(KeyCode::Backspace, none, "pop-char", vec!["item".into()]); ia.bind_any_char("append-char", vec!["item".into()]); set.insert(ModeKey::ItemAdd, Arc::new(ia)); // ── Export prompt ──────────────────────────────────────────────── let mut ep = Keymap::new(); - ep.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); - ep.bind(KeyCode::Enter, none, "commit-export"); + ep.bind_seq(KeyCode::Esc, none, vec![ + ("clear-buffer", vec!["export".into()]), + ("enter-mode", vec!["normal".into()]), + ]); + ep.bind_seq(KeyCode::Enter, none, vec![ + ("commit-export", vec![]), + ("clear-buffer", vec!["export".into()]), + ]); ep.bind_args(KeyCode::Backspace, none, "pop-char", vec!["export".into()]); ep.bind_any_char("append-char", vec!["export".into()]); set.insert(ModeKey::ExportPrompt, Arc::new(ep)); // ── Command mode ───────────────────────────────────────────────── let mut cm = Keymap::new(); - cm.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); - cm.bind(KeyCode::Enter, none, "execute-command"); + cm.bind_seq(KeyCode::Esc, none, vec![ + ("clear-buffer", vec!["command".into()]), + ("enter-mode", vec!["normal".into()]), + ]); + cm.bind_seq(KeyCode::Enter, none, vec![ + ("execute-command", vec![]), + ("clear-buffer", vec!["command".into()]), + ]); cm.bind(KeyCode::Backspace, none, "command-mode-backspace"); cm.bind_any_char("append-char", vec!["command".into()]); set.insert(ModeKey::CommandMode, Arc::new(cm));