feat(ui): implement AbortChain and CleanEmptyRecords effects

Implement AbortChain and CleanEmptyRecords effects to allow
short-circuiting effect batches and purging cells with empty coordinates.
Update the App struct to support aborting effects during the application of
an effect batch.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-26B-A4B-it-UD-Q5_K_XL.gguf)
This commit is contained in:
Edward Langley
2026-04-15 23:42:44 -07:00
parent f272a9d459
commit 489e2805e8
2 changed files with 225 additions and 15 deletions
+92 -3
View File
@@ -229,6 +229,12 @@ pub struct App {
/// Current grid layout, derived from model + view + drill_state.
/// Rebuilt via `rebuild_layout()` after state changes.
pub layout: GridLayout,
/// When set to true by an effect during `apply_effects`, the remaining
/// effects in the batch are skipped. The flag is reset at the start of
/// every `apply_effects` call. Use via the `AbortChain` effect — this is
/// the mechanism by which e.g. "advance at bottom-right" short-circuits
/// the trailing `EnterEditAtCursor` in a `CommitAndAdvance` chain.
pub abort_effects: bool,
keymap_set: KeymapSet,
}
@@ -272,6 +278,7 @@ impl App {
buffers: HashMap::new(),
transient_keymap: None,
layout,
abort_effects: false,
keymap_set: KeymapSet::default_keymaps(),
}
}
@@ -338,7 +345,8 @@ impl App {
visible_rows: (self.term_height as usize).saturating_sub(8),
visible_cols: {
let (fmt_comma, fmt_decimals) = parse_number_format(&view.number_format);
let col_widths = compute_col_widths(&self.workbook.model, layout, fmt_comma, fmt_decimals);
let col_widths =
compute_col_widths(&self.workbook.model, layout, fmt_comma, fmt_decimals);
let row_header_width = compute_row_header_width(layout);
compute_visible_cols(
&col_widths,
@@ -353,8 +361,16 @@ impl App {
}
pub fn apply_effects(&mut self, effects: Vec<Box<dyn super::effect::Effect>>) {
self.abort_effects = false;
for effect in effects {
effect.apply(self);
if self.abort_effects {
// AbortChain (or another abort-setting effect) requested
// that the rest of this batch be skipped. Reset the flag so
// the next dispatch starts clean.
self.abort_effects = false;
break;
}
}
self.rebuild_layout();
}
@@ -909,6 +925,73 @@ mod tests {
app
}
/// improvise-3zq (bug #2): `AddRecordRow` creates a cell with an empty
/// `CellKey` when no Page-axis categories supply coords — that cell
/// serialises as ` = 0` in .improv and re-appears on every records
/// toggle. Leaving records mode must clean up any such meaningless
/// records (inverse of the `SortData` that runs on entry).
#[test]
fn leaving_records_mode_cleans_empty_key_cells() {
use crate::model::cell::{CellKey, CellValue};
let mut app = records_model_with_two_rows();
// Simulate Tab-at-bottom-right having produced an empty-key cell.
app.workbook
.model
.set_cell(CellKey::new(vec![]), CellValue::Number(0.0));
assert!(
app.workbook
.model
.data
.iter_cells()
.any(|(k, _)| k.0.is_empty()),
"setup: empty-key cell should be present"
);
// Leave records mode via R.
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
.unwrap();
assert!(
!app.layout.is_records_mode(),
"setup: should have left records mode"
);
assert!(
!app.workbook
.model
.data
.iter_cells()
.any(|(k, _)| k.0.is_empty()),
"empty-key records should be cleaned when leaving records mode"
);
}
/// improvise-3zq (bug #1): Enter on the bottom-right cell of records
/// view should commit and leave edit mode. Previously `CommitAndAdvance`
/// pushed an `EnterEditAtCursor` effect unconditionally, so the cursor
/// stayed put and we re-entered editing on the same cell.
#[test]
fn enter_at_bottom_right_of_records_view_exits_editing() {
let mut app = records_model_with_two_rows();
let last_row = app.layout.row_count() - 1;
let last_col = app.layout.col_count() - 1;
app.workbook.active_view_mut().selected = (last_row, last_col);
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
.unwrap();
assert!(app.mode.is_editing(), "setup: should be editing");
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
.unwrap();
assert!(
!app.mode.is_editing(),
"Enter at bottom-right should exit editing, got {:?}",
app.mode
);
assert!(
matches!(app.mode, AppMode::RecordsNormal),
"should return to RecordsNormal, got {:?}",
app.mode
);
}
/// improvise-hmu: TAB on the bottom-right cell of records view should
/// insert a new record below and move to the first cell of the new row
/// in edit mode.
@@ -963,7 +1046,8 @@ mod tests {
("Month".to_string(), "Jan".to_string()),
("Region".to_string(), "East".to_string()),
]);
wb.model.set_cell(record_key.clone(), CellValue::Number(1.0));
wb.model
.set_cell(record_key.clone(), CellValue::Number(1.0));
let mut app = App::new(wb, None);
app.handle_key(KeyEvent::new(KeyCode::Char('>'), KeyModifiers::NONE))
@@ -1034,7 +1118,12 @@ mod tests {
.unwrap();
assert!(
!app.workbook.model.category("Region").unwrap().items.contains_key(""),
!app.workbook
.model
.category("Region")
.unwrap()
.items
.contains_key(""),
"records-mode edits should not create empty category items"
);
}