From fc2f00bd501a1ffa6fc8500eb3c1609c2bc2c2b1 Mon Sep 17 00:00:00 2001
From: nihilist-narwhal <240798405+nihilist-narwhal@users.noreply.github.com>
Date: Sat, 10 Oct 2026 04:24:42 -0500
Subject: [PATCH] Recalc: a range ending in INDEX depends on the cells it spans
=SUM(C3:INDEX(B9:C11,2,2)) adds C3:C10, but only C3 and B9:C11 were
recorded as its precedents, so typing into C5 left the total unchanged
until a full recalculation. A range whose ends are function results now
depends on the box spanned by the references inside it, which contains
any cell those functions can return.
---
crates/calc/src/eval.rs | 14 ++++++++++++++
crates/engine/src/tests.rs | 8 ++++++++
2 files changed, 22 insertions(+)
diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs
index abef54ffd..c010020a6 100644
--- a/crates/calc/src/eval.rs
+++ b/crates/calc/src/eval.rs
@@ -1337,6 +1337,20 @@ pub fn precedents(wb: &Workbook, sheet: usize, e: &Expr) -> (Vec, bool) {
}
Expr::Name(_) | Expr::Struct(_) => dynamic = true,
Expr::Call(n, _) if gridcraft_functions::lookup(n).is_none() && !is_special(n) => dynamic = true,
+ // A range ending in a function's result (`A1:INDEX(B1:B9,n)`) covers the cells between
+ // the references inside it, not only those references.
+ Expr::Binary(BinOp::Range, a, b) if !matches!((&**a, &**b), (Expr::Ref(_), Expr::Ref(_))) => {
+ let mut inner = precedents(wb, sheet, a).0;
+ inner.extend(precedents(wb, sheet, b).0);
+ let mut boxes: Vec = Vec::new();
+ for area in inner {
+ match boxes.iter_mut().find(|x| x.sheet == area.sheet) {
+ Some(x) => x.range = x.range.union(&area.range),
+ None => boxes.push(area),
+ }
+ }
+ out.extend(boxes);
+ }
_ => {}
});
(out, dynamic)
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
index 361fbce48..9fe3fa8fa 100644
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -165,6 +165,14 @@ fn replacing_a_spilling_formula_clears_its_spill() {
assert_eq!((v(&s, "A2"), v(&s, "B1")), (Value::Empty, Value::Number(0.0)));
}
+#[test]
+fn range_ending_in_index_recalculates() {
+ let mut s = s();
+ s.execute("cell.set", json!({"cell": "B6", "input": "=SUM(C3:INDEX(B9:C11,2,2))"})).unwrap();
+ s.execute("cell.set", json!({"cell": "C5", "input": "2"})).unwrap();
+ assert_eq!(v(&s, "B6"), Value::Number(2.0));
+}
+
#[test]
fn every_command_survives_empty_params() {
let mut s = Session::new();