From dcc5434bc83ddc40c239664000217e8a88094d26 Mon Sep 17 00:00:00 2001 From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com> Date: Sat, 10 Oct 2026 04:27:44 -0500 Subject: [PATCH] Recalc: structural edits don't leave old spills behind Inserting or deleting rows and columns moved formula cells but not the values they had spilled, and the full recalculation that follows reconciled spills only at the formulas' new positions. =SEQUENCE(3) in A5 with row 1 deleted showed #SPILL! in A4 (blocked by its own old values in A6:A7); deleting the anchor's row left 2 and 3 behind. A full recalculation now drops all spills and lays them out again from the formulas. Fixes #133. --- crates/calc/src/recalc.rs | 10 ++++++++++ crates/engine/src/tests.rs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/crates/calc/src/recalc.rs b/crates/calc/src/recalc.rs index 584339534..32d588705 100644 --- a/crates/calc/src/recalc.rs +++ b/crates/calc/src/recalc.rs @@ -345,6 +345,16 @@ impl Calc { /// Recalculates every formula (F9 / Ctrl+Alt+F9, and after loading). pub fn recalc_all(&mut self, wb: &mut Workbook) { self.rebuild(wb); + // Spills are laid out again from the formulas where they are now (a structural edit moves + // or deletes the formulas, not the values they spilled). + for i in 0..wb.sheets.len() { + if wb.sheet(i).is_some_and(|s| !s.spill_ranges.is_empty() || !s.spill.is_empty()) + && let Some(sh) = wb.sheet_mut(i) + { + sh.spill.clear(); + sh.spill_ranges.clear(); + } + } let all: Vec = self.graph.nodes.keys().copied().collect(); self.run(wb, all, true); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 9fe3fa8fa..9343faeda 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -173,6 +173,17 @@ fn range_ending_in_index_recalculates() { assert_eq!(v(&s, "B6"), Value::Number(2.0)); } +#[test] +fn structural_edits_move_spills_with_their_formula() { + let mut s = s(); + s.execute("cell.set", json!({"cell": "A5", "input": "=SEQUENCE(3)"})).unwrap(); + s.execute("home.deleteRows", json!({"rows": "1:1"})).unwrap(); + let col = |s: &Session| (4..=7).map(|r| v(s, &format!("A{r}"))).collect::>(); + assert_eq!(col(&s), [Value::Number(1.0), Value::Number(2.0), Value::Number(3.0), Value::Empty]); + s.execute("home.deleteRows", json!({"rows": "4:4"})).unwrap(); + assert_eq!(col(&s), [Value::Empty, Value::Empty, Value::Empty, Value::Empty]); +} + #[test] fn every_command_survives_empty_params() { let mut s = Session::new();