feat(command): add keymap inheritance and sequence bindings

Implement keymap inheritance and sequence bindings.

- Added `parent` field to `Keymap` to support Emacs-style inheritance.
- Implemented `lookup` in `Keymap` to fall through to parent keymaps.
- Added `bind_seq` to allow multiple commands to be bound to a single key
  pattern.
- Refactored existing keymaps to use sequences for common patterns like
  Esc/Enter/Tab to clear buffers and change modes.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-04-09 14:24:38 -07:00
parent c8b9d29690
commit c3fb8669c2

View File

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