From ab927753576af05dce3a58aeea6c0a9d97e77176 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Sun, 5 Apr 2026 12:35:40 -0700 Subject: [PATCH] feat(command): add smart edit-or-drill for aggregated cells Introduce EditOrDrill command that intelligently handles editing based on cell type. When cursor is on an aggregated pivot cell (categories on Axis::None, no records mode), it drills into the cell. Otherwise, it enters edit mode with the current displayed value. The 'i' and 'a' keys now trigger edit-or-drill instead of enter-edit-mode. Aggregated cells are styled in italic to signal that drilling is required for editing. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M) --- src/command/cmd.rs | 35 +++++++++++++++++++++++++++++++++++ src/command/keymap.rs | 6 +++--- src/ui/grid.rs | 11 ++++++++++- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/command/cmd.rs b/src/command/cmd.rs index 3d4a9fa..ff82d89 100644 --- a/src/command/cmd.rs +++ b/src/command/cmd.rs @@ -633,6 +633,40 @@ impl Cmd for EnterEditMode { } } +/// Smart dispatch for i/a: if the cursor is on an aggregated pivot cell +/// (categories on `Axis::None`, no records mode), drill into it instead of +/// editing. Otherwise enter edit mode with the current displayed value. +#[derive(Debug)] +pub struct EditOrDrill; +impl Cmd for EditOrDrill { + fn name(&self) -> &'static str { + "edit-or-drill" + } + fn execute(&self, ctx: &CmdContext) -> Vec> { + let is_aggregated = ctx.records_col.is_none() && !ctx.none_cats.is_empty(); + if is_aggregated { + let Some(key) = ctx.cell_key.clone() else { + return vec![effect::set_status( + "cannot drill — no cell at cursor", + )]; + }; + return DrillIntoCell { key }.execute(ctx); + } + // Edit path: prefer records display value (includes pending edits), + // else the underlying cell's stored value. + let initial_value = if let Some(v) = &ctx.records_value { + v.clone() + } else { + ctx.cell_key + .as_ref() + .and_then(|k| ctx.model.get_cell(k).cloned()) + .map(|v| v.to_string()) + .unwrap_or_default() + }; + EnterEditMode { initial_value }.execute(ctx) + } +} + /// Typewriter-style advance: move down, wrap to top of next column at bottom. #[derive(Debug)] pub struct EnterAdvance { @@ -2341,6 +2375,7 @@ pub fn default_registry() -> CmdRegistry { })) }, ); + r.register_nullary(|| Box::new(EditOrDrill)); r.register_nullary(|| Box::new(EnterExportPrompt)); r.register_nullary(|| Box::new(EnterFormulaEdit)); r.register_nullary(|| Box::new(EnterTileSelect)); diff --git a/src/command/keymap.rs b/src/command/keymap.rs index 1c150cf..62c3cff 100644 --- a/src/command/keymap.rs +++ b/src/command/keymap.rs @@ -331,9 +331,9 @@ impl KeymapSet { ); normal.bind(KeyCode::Tab, none, "cycle-panel-focus"); - // Editing entry - normal.bind(KeyCode::Char('i'), none, "enter-edit-mode"); - normal.bind(KeyCode::Char('a'), none, "enter-edit-mode"); + // Editing entry — i/a drill into aggregated cells, else edit + normal.bind(KeyCode::Char('i'), none, "edit-or-drill"); + normal.bind(KeyCode::Char('a'), none, "edit-or-drill"); normal.bind(KeyCode::Enter, none, "enter-advance"); normal.bind(KeyCode::Char('e'), ctrl, "enter-export-prompt"); diff --git a/src/ui/grid.rs b/src/ui/grid.rs index b5099fc..60229fd 100644 --- a/src/ui/grid.rs +++ b/src/ui/grid.rs @@ -402,7 +402,13 @@ impl<'a> GridWidget<'a> { .to_lowercase() .contains(&self.search_query.to_lowercase()); - let cell_style = if is_selected { + // Aggregated cells (pivot view with hidden dims) are + // not directly editable — shown in italic to signal + // "drill to edit". Records mode cells are always + // directly editable, as are plain pivot cells. + let is_aggregated = !layout.is_records_mode() + && !layout.none_cats.is_empty(); + let mut cell_style = if is_selected { Style::default() .fg(Color::Black) .bg(Color::Cyan) @@ -421,6 +427,9 @@ impl<'a> GridWidget<'a> { } else { Style::default() }; + if is_aggregated { + cell_style = cell_style.add_modifier(Modifier::ITALIC); + } buf.set_string( x,