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)
This commit is contained in:
Edward Langley
2026-04-05 12:35:40 -07:00
parent 94bc3ca282
commit ab92775357
3 changed files with 48 additions and 4 deletions

View File

@ -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<Box<dyn Effect>> {
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. /// Typewriter-style advance: move down, wrap to top of next column at bottom.
#[derive(Debug)] #[derive(Debug)]
pub struct EnterAdvance { 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(EnterExportPrompt));
r.register_nullary(|| Box::new(EnterFormulaEdit)); r.register_nullary(|| Box::new(EnterFormulaEdit));
r.register_nullary(|| Box::new(EnterTileSelect)); r.register_nullary(|| Box::new(EnterTileSelect));

View File

@ -331,9 +331,9 @@ impl KeymapSet {
); );
normal.bind(KeyCode::Tab, none, "cycle-panel-focus"); normal.bind(KeyCode::Tab, none, "cycle-panel-focus");
// Editing entry // Editing entry — i/a drill into aggregated cells, else edit
normal.bind(KeyCode::Char('i'), none, "enter-edit-mode"); normal.bind(KeyCode::Char('i'), none, "edit-or-drill");
normal.bind(KeyCode::Char('a'), none, "enter-edit-mode"); normal.bind(KeyCode::Char('a'), none, "edit-or-drill");
normal.bind(KeyCode::Enter, none, "enter-advance"); normal.bind(KeyCode::Enter, none, "enter-advance");
normal.bind(KeyCode::Char('e'), ctrl, "enter-export-prompt"); normal.bind(KeyCode::Char('e'), ctrl, "enter-export-prompt");

View File

@ -402,7 +402,13 @@ impl<'a> GridWidget<'a> {
.to_lowercase() .to_lowercase()
.contains(&self.search_query.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() Style::default()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Cyan) .bg(Color::Cyan)
@ -421,6 +427,9 @@ impl<'a> GridWidget<'a> {
} else { } else {
Style::default() Style::default()
}; };
if is_aggregated {
cell_style = cell_style.add_modifier(Modifier::ITALIC);
}
buf.set_string( buf.set_string(
x, x,