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:
File diff suppressed because it is too large
Load Diff
@ -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 ────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user