feat: add records-mode drill-down with staged edits

Introduce records-mode drill-down functionality that allows users to
edit individual records without immediately modifying the underlying model.

Key changes:
- Added DrillState struct to hold frozen records snapshot and pending edits
- New effects: StartDrill, ApplyAndClearDrill, SetDrillPendingEdit
- Extended CmdContext with records_col and records_value for records mode
- CommitCellEdit now stages edits in pending_edits when in records mode
- DrillIntoCell captures a snapshot before switching to drill view
- GridLayout supports frozen records for stable view during edits
- GridWidget renders with drill_state for pending edit display

In records mode, edits are staged and only applied to the model when
the user navigates away or commits. This prevents data loss and allows
batch editing of records.

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 11:45:36 -07:00
parent 19645a34cf
commit 78df3a4949
6 changed files with 311 additions and 12 deletions

View File

@ -13,6 +13,20 @@ use crate::model::Model;
use crate::persistence;
use crate::view::GridLayout;
/// Drill-down state: frozen record snapshot + pending edits that have not
/// yet been applied to the model.
#[derive(Debug, Clone, Default)]
pub struct DrillState {
/// Frozen snapshot of records shown in the drill view.
pub records: Vec<(
crate::model::cell::CellKey,
crate::model::cell::CellValue,
)>,
/// Pending edits keyed by (record_idx, column_name) → new string value.
/// column_name is either "Value" or a category name.
pub pending_edits: std::collections::HashMap<(usize, String), String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppMode {
Normal,
@ -72,6 +86,11 @@ pub struct App {
pub view_back_stack: Vec<String>,
/// Views that were "back-ed" from, available for forward navigation (`>`).
pub view_forward_stack: Vec<String>,
/// Frozen records list for the drill view. When present, this is the
/// snapshot that records-mode layouts iterate — records don't disappear
/// when filters would change. Pending edits are stored alongside and
/// applied to the model on commit/navigate-away.
pub drill_state: Option<DrillState>,
/// Named text buffers for text-entry modes
pub buffers: HashMap<String, String>,
/// Transient keymap for Emacs-style prefix key sequences (g→gg, y→yy, etc.)
@ -101,6 +120,7 @@ impl App {
tile_cat_idx: 0,
view_back_stack: Vec::new(),
view_forward_stack: Vec::new(),
drill_state: None,
buffers: HashMap::new(),
transient_keymap: None,
keymap_set: KeymapSet::default_keymaps(),
@ -109,7 +129,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 frozen_records = self.drill_state.as_ref().map(|s| s.records.clone());
let layout = GridLayout::with_frozen_records(&self.model, view, frozen_records);
let (sel_row, sel_col) = view.selected;
CmdContext {
model: &self.model,
@ -135,6 +156,21 @@ impl App {
none_cats: layout.none_cats.clone(),
view_back_stack: self.view_back_stack.clone(),
view_forward_stack: self.view_forward_stack.clone(),
records_col: if layout.is_records_mode() {
Some(layout.col_label(sel_col))
} else {
None
},
records_value: if layout.is_records_mode() {
// Check pending edits first, then fall back to original
let col_name = layout.col_label(sel_col);
let pending = self.drill_state.as_ref().and_then(|s| {
s.pending_edits.get(&(sel_row, col_name.clone())).cloned()
});
pending.or_else(|| layout.records_display(sel_row, sel_col))
} else {
None
},
key_code: key,
}
}