refactor(command): pre-resolve cell key and grid dimensions in CmdContext

Refactor command system to pre-resolve cell key and grid dimensions
in CmdContext, eliminating repeated GridLayout construction.

Key changes:
- Add cell_key, row_count, col_count to CmdContext
- Replace generic CmdRegistry::register with
  register/register_pure/register_nullary
- Cell commands (clear-cell, yank, paste) now take explicit CellKey
- Update keymap dispatch to use new interactive() method
- Special-case "search" buffer in SetBuffer effect
- Update tests to populate new context fields

This reduces code duplication and makes command execution more
efficient by computing layout once at context creation time.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
Edward Langley
2026-04-04 12:14:03 -07:00
parent 649d80cb35
commit 35946afc91
4 changed files with 603 additions and 313 deletions

File diff suppressed because it is too large Load Diff

View File

@ -165,7 +165,7 @@ impl Keymap {
let binding = self.lookup(key, mods)?;
match binding {
Binding::Cmd { name, args } => {
let cmd = registry.parse(name, args).ok()?;
let cmd = registry.interactive(name, args, ctx).ok()?;
Some(cmd.execute(ctx))
}
Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]),
@ -267,7 +267,7 @@ impl KeymapSet {
normal.bind_args(KeyCode::Char('u'), ctrl, "scroll-rows", vec!["-5".into()]);
// Cell operations
normal.bind(KeyCode::Char('x'), none, "clear-selected-cell");
normal.bind(KeyCode::Char('x'), none, "clear-cell");
normal.bind(KeyCode::Char('p'), none, "paste");
// View

View File

@ -11,6 +11,7 @@ use crate::import::wizard::ImportWizard;
use crate::model::cell::CellValue;
use crate::model::Model;
use crate::persistence;
use crate::view::GridLayout;
#[derive(Debug, Clone, PartialEq)]
pub enum AppMode {
@ -101,6 +102,8 @@ impl App {
pub fn cmd_context(&self, key: KeyCode, mods: KeyModifiers) -> CmdContext<'_> {
let view = self.model.active_view();
let layout = GridLayout::new(&self.model, view);
let (sel_row, sel_col) = view.selected;
CmdContext {
model: &self.model,
mode: &self.mode,
@ -120,6 +123,9 @@ impl App {
cat_panel_cursor: self.cat_panel_cursor,
view_panel_cursor: self.view_panel_cursor,
tile_cat_idx: self.tile_cat_idx,
cell_key: layout.cell_key(sel_row, sel_col),
row_count: layout.row_count(),
col_count: layout.col_count(),
key_code: key,
key_modifiers: mods,
}
@ -220,11 +226,26 @@ mod tests {
app.apply_effects(effects);
}
fn enter_advance_cmd(app: &App) -> crate::command::cmd::EnterAdvance {
use crate::command::cmd::CursorState;
let view = app.model.active_view();
let cursor = CursorState {
row: view.selected.0,
col: view.selected.1,
row_count: 3,
col_count: 2,
row_offset: 0,
col_offset: 0,
};
crate::command::cmd::EnterAdvance { cursor }
}
#[test]
fn enter_advance_moves_down_within_column() {
let mut app = two_col_model();
app.model.active_view_mut().selected = (0, 0);
run_cmd(&mut app, &crate::command::cmd::EnterAdvance);
let cmd = enter_advance_cmd(&app);
run_cmd(&mut app, &cmd);
assert_eq!(app.model.active_view().selected, (1, 0));
}
@ -233,7 +254,8 @@ mod tests {
let mut app = two_col_model();
// row_max = 2 (A,B,C), col 0 → should wrap to (0, 1)
app.model.active_view_mut().selected = (2, 0);
run_cmd(&mut app, &crate::command::cmd::EnterAdvance);
let cmd = enter_advance_cmd(&app);
run_cmd(&mut app, &cmd);
assert_eq!(app.model.active_view().selected, (0, 1));
}
@ -241,7 +263,8 @@ mod tests {
fn enter_advance_stays_at_bottom_right() {
let mut app = two_col_model();
app.model.active_view_mut().selected = (2, 1);
run_cmd(&mut app, &crate::command::cmd::EnterAdvance);
let cmd = enter_advance_cmd(&app);
run_cmd(&mut app, &cmd);
assert_eq!(app.model.active_view().selected, (2, 1));
}

View File

@ -289,7 +289,12 @@ pub struct SetBuffer {
}
impl Effect for SetBuffer {
fn apply(&self, app: &mut App) {
app.buffers.insert(self.name.clone(), self.value.clone());
// "search" is special — it writes to search_query for backward compat
if self.name == "search" {
app.search_query = self.value.clone();
} else {
app.buffers.insert(self.name.clone(), self.value.clone());
}
}
}