refactor(cmd): introduce command algebra with Binding::Sequence and unified primitives

Add Binding::Sequence to keymap for composing commands, then use it
and parameterization to eliminate redundant command structs:

- Unify MoveSelection/JumpToEdge/ScrollRows/PageScroll into Move
- Merge ToggleGroupUnderCursor + ToggleColGroupUnderCursor → ToggleGroupAtCursor
- Merge CommitCellEdit + CommitAndAdvanceRight → CommitAndAdvance
- Merge CycleAxisForTile + SetAxisForTile → TileAxisOp
- Merge ViewBackCmd + ViewForwardCmd → ViewNavigate
- Delete SearchAppendChar/SearchPopChar (reuse AppendChar/PopChar with "search")
- Replace SaveAndQuit/OpenRecordRow with keymap sequences
- Extract commit_add_from_buffer helper for CommitCategoryAdd/CommitItemAdd
- Add algebraic law tests (idempotence, involution, associativity)

https://claude.ai/code/session_01Y9X6VKyZAW3xo1nfThDRYU
This commit is contained in:
Claude
2026-04-07 06:48:34 +00:00
committed by Edward Langley
parent b3d80e2986
commit 90c971539c
2 changed files with 450 additions and 397 deletions

File diff suppressed because it is too large Load Diff

View File

@ -72,6 +72,8 @@ pub enum Binding {
}, },
/// A prefix sub-keymap (Emacs-style). /// A prefix sub-keymap (Emacs-style).
Prefix(Arc<Keymap>), Prefix(Arc<Keymap>),
/// A sequence of commands executed in order, concatenating their effects.
Sequence(Vec<(&'static str, Vec<String>)>),
} }
/// 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).
@ -121,6 +123,17 @@ impl Keymap {
.insert(KeyPattern::Key(key, mods), Binding::Prefix(sub)); .insert(KeyPattern::Key(key, mods), Binding::Prefix(sub));
} }
/// Bind a key to a sequence of commands (executed in order).
pub fn bind_seq(
&mut self,
key: KeyCode,
mods: KeyModifiers,
steps: Vec<(&'static str, Vec<String>)>,
) {
self.bindings
.insert(KeyPattern::Key(key, mods), Binding::Sequence(steps));
}
/// Bind a catch-all for any Char key. /// Bind a catch-all for any Char key.
pub fn bind_any_char(&mut self, name: &'static str, args: Vec<String>) { pub fn bind_any_char(&mut self, name: &'static str, args: Vec<String>) {
self.bindings self.bindings
@ -173,6 +186,14 @@ impl Keymap {
Some(cmd.execute(ctx)) Some(cmd.execute(ctx))
} }
Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]), Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]),
Binding::Sequence(steps) => {
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
for (name, args) in steps {
let cmd = registry.interactive(name, args, ctx).ok()?;
effects.extend(cmd.execute(ctx));
}
Some(effects)
}
} }
} }
} }
@ -360,7 +381,14 @@ impl KeymapSet {
// Drill into aggregated cell / view history / add row // Drill into aggregated cell / view history / add row
normal.bind(KeyCode::Char('>'), none, "drill-into-cell"); normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
normal.bind(KeyCode::Char('<'), none, "view-back"); normal.bind(KeyCode::Char('<'), none, "view-back");
normal.bind(KeyCode::Char('o'), none, "open-record-row"); normal.bind_seq(
KeyCode::Char('o'),
none,
vec![
("add-record-row", vec![]),
("enter-edit-at-cursor", vec![]),
],
);
// Records mode toggle and prune toggle // Records mode toggle and prune toggle
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode"); normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
@ -384,7 +412,11 @@ impl KeymapSet {
normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map)); normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map));
let mut z_map = Keymap::new(); let mut z_map = Keymap::new();
z_map.bind(KeyCode::Char('Z'), none, "save-and-quit"); z_map.bind_seq(
KeyCode::Char('Z'),
none,
vec![("save", vec![]), ("force-quit", vec![])],
);
normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map)); normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map));
set.insert(ModeKey::Normal, Arc::new(normal)); set.insert(ModeKey::Normal, Arc::new(normal));
@ -664,8 +696,13 @@ impl KeymapSet {
let mut sm = Keymap::new(); let mut sm = Keymap::new();
sm.bind(KeyCode::Esc, none, "exit-search-mode"); sm.bind(KeyCode::Esc, none, "exit-search-mode");
sm.bind(KeyCode::Enter, none, "exit-search-mode"); sm.bind(KeyCode::Enter, none, "exit-search-mode");
sm.bind(KeyCode::Backspace, none, "search-pop-char"); sm.bind_args(
sm.bind_any_char("search-append-char", vec![]); KeyCode::Backspace,
none,
"pop-char",
vec!["search".into()],
);
sm.bind_any_char("append-char", vec!["search".into()]);
set.insert(ModeKey::SearchMode, Arc::new(sm)); set.insert(ModeKey::SearchMode, Arc::new(sm));
// ── Import wizard ──────────────────────────────────────────────── // ── Import wizard ────────────────────────────────────────────────