@pixerate/editor 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +30 -0
  2. package/THIRD_PARTY_NOTICES.md +11 -0
  3. package/dist/agent/index.cjs +174 -11
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +18 -4
  6. package/dist/agent/index.d.ts +18 -4
  7. package/dist/agent/index.js +167 -4
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/{arrangeShortcuts-B8O3f8-P.d.cts → arrange-BICutYPk.d.cts} +243 -29
  10. package/dist/{arrangeShortcuts-sLlD2rSX.d.ts → arrange-BICutYPk.d.ts} +243 -29
  11. package/dist/builtins-CdPcLwwF.d.cts +176 -0
  12. package/dist/builtins-Chj6zv_O.d.ts +176 -0
  13. package/dist/canvas/index.cjs +3917 -19
  14. package/dist/canvas/index.cjs.map +1 -1
  15. package/dist/canvas/index.d.cts +1232 -7
  16. package/dist/canvas/index.d.ts +1232 -7
  17. package/dist/canvas/index.js +3916 -18
  18. package/dist/canvas/index.js.map +1 -1
  19. package/dist/chunk-3CC7OPUQ.cjs +831 -0
  20. package/dist/chunk-3CC7OPUQ.cjs.map +1 -0
  21. package/dist/{chunk-7WISYZFO.cjs → chunk-66IMF6OD.cjs} +174 -67
  22. package/dist/chunk-66IMF6OD.cjs.map +1 -0
  23. package/dist/{chunk-AL6JMGWW.cjs → chunk-CS6NZWWA.cjs} +24 -24
  24. package/dist/{chunk-AL6JMGWW.cjs.map → chunk-CS6NZWWA.cjs.map} +1 -1
  25. package/dist/{chunk-C7P22VIP.cjs → chunk-EWCOFSCQ.cjs} +17 -6
  26. package/dist/chunk-EWCOFSCQ.cjs.map +1 -0
  27. package/dist/chunk-HDYMLS3X.cjs +409 -0
  28. package/dist/chunk-HDYMLS3X.cjs.map +1 -0
  29. package/dist/{chunk-V3CHHWOE.cjs → chunk-HNDSPZ5T.cjs} +4 -4
  30. package/dist/{chunk-V3CHHWOE.cjs.map → chunk-HNDSPZ5T.cjs.map} +1 -1
  31. package/dist/{chunk-X4N4WUKB.cjs → chunk-IFHKKVXH.cjs} +150 -38
  32. package/dist/chunk-IFHKKVXH.cjs.map +1 -0
  33. package/dist/chunk-KRGXUQ2Y.js +1696 -0
  34. package/dist/chunk-KRGXUQ2Y.js.map +1 -0
  35. package/dist/{chunk-PO6A6QQH.cjs → chunk-LN2POBHT.cjs} +742 -970
  36. package/dist/chunk-LN2POBHT.cjs.map +1 -0
  37. package/dist/chunk-MR2764X2.js +831 -0
  38. package/dist/chunk-MR2764X2.js.map +1 -0
  39. package/dist/{chunk-WSQKHXW7.cjs → chunk-NQF32I6Z.cjs} +1209 -457
  40. package/dist/chunk-NQF32I6Z.cjs.map +1 -0
  41. package/dist/{chunk-NAXVVS5I.js → chunk-QJSOZFCZ.js} +2 -2
  42. package/dist/{chunk-UAKDG62Z.js → chunk-SWE3NPOU.js} +116 -4
  43. package/dist/chunk-SWE3NPOU.js.map +1 -0
  44. package/dist/chunk-TXXNBDQX.cjs +1696 -0
  45. package/dist/chunk-TXXNBDQX.cjs.map +1 -0
  46. package/dist/chunk-UBCFIABU.js +409 -0
  47. package/dist/chunk-UBCFIABU.js.map +1 -0
  48. package/dist/{chunk-ECCCYKGL.js → chunk-UKI64KUD.js} +147 -40
  49. package/dist/chunk-UKI64KUD.js.map +1 -0
  50. package/dist/{chunk-QGYDWPRM.js → chunk-WON34HKM.js} +2 -2
  51. package/dist/{chunk-RR7F5GUM.js → chunk-X7HXL2SB.js} +941 -189
  52. package/dist/chunk-X7HXL2SB.js.map +1 -0
  53. package/dist/{chunk-IXHSYOVY.js → chunk-XQFHLT53.js} +638 -866
  54. package/dist/chunk-XQFHLT53.js.map +1 -0
  55. package/dist/{chunk-FH6F67DN.js → chunk-ZRNHGAQT.js} +17 -6
  56. package/dist/{chunk-FH6F67DN.js.map → chunk-ZRNHGAQT.js.map} +1 -1
  57. package/dist/commands/index.cjs +352 -4
  58. package/dist/commands/index.cjs.map +1 -1
  59. package/dist/commands/index.d.cts +37 -108
  60. package/dist/commands/index.d.ts +37 -108
  61. package/dist/commands/index.js +351 -3
  62. package/dist/commands/index.js.map +1 -1
  63. package/dist/compare/index.cjs +3 -3
  64. package/dist/compare/index.js +2 -2
  65. package/dist/{controller-BIJiNoVs.d.cts → controller-CxXGOEmy.d.cts} +209 -13
  66. package/dist/{controller-BIJiNoVs.d.ts → controller-CxXGOEmy.d.ts} +209 -13
  67. package/dist/export/index.cjs +36 -0
  68. package/dist/export/index.cjs.map +1 -0
  69. package/dist/export/index.d.cts +243 -0
  70. package/dist/export/index.d.ts +243 -0
  71. package/dist/export/index.js +36 -0
  72. package/dist/export/index.js.map +1 -0
  73. package/dist/extensions/index.cjs +54 -6
  74. package/dist/extensions/index.cjs.map +1 -1
  75. package/dist/extensions/index.d.cts +7 -2
  76. package/dist/extensions/index.d.ts +7 -2
  77. package/dist/extensions/index.js +59 -11
  78. package/dist/{index-7a6bHTLy.d.ts → index-B273Zllk.d.ts} +211 -207
  79. package/dist/{index-Csd5qI8u.d.cts → index-jcJwmBYt.d.cts} +211 -207
  80. package/dist/index.cjs +146 -32
  81. package/dist/index.cjs.map +1 -1
  82. package/dist/index.d.cts +50 -14
  83. package/dist/index.d.ts +50 -14
  84. package/dist/index.js +138 -24
  85. package/dist/index.js.map +1 -1
  86. package/dist/registry-CED5C0lA.d.cts +204 -0
  87. package/dist/registry-CED5C0lA.d.ts +204 -0
  88. package/dist/{registry-D32LpIc4.d.cts → registry-D0GADKc7.d.cts} +1 -1
  89. package/dist/{registry-evKTP4ls.d.ts → registry-DhVUZioj.d.ts} +1 -1
  90. package/dist/serializers/index.cjs +6 -4
  91. package/dist/serializers/index.cjs.map +1 -1
  92. package/dist/serializers/index.d.cts +15 -4
  93. package/dist/serializers/index.d.ts +15 -4
  94. package/dist/serializers/index.js +5 -3
  95. package/dist/spreadsheet/index.cjs +33 -3
  96. package/dist/spreadsheet/index.cjs.map +1 -1
  97. package/dist/spreadsheet/index.d.cts +106 -3
  98. package/dist/spreadsheet/index.d.ts +106 -3
  99. package/dist/spreadsheet/index.js +32 -2
  100. package/package.json +10 -1
  101. package/dist/chunk-7WISYZFO.cjs.map +0 -1
  102. package/dist/chunk-C7P22VIP.cjs.map +0 -1
  103. package/dist/chunk-ECCCYKGL.js.map +0 -1
  104. package/dist/chunk-IXHSYOVY.js.map +0 -1
  105. package/dist/chunk-PO6A6QQH.cjs.map +0 -1
  106. package/dist/chunk-RR7F5GUM.js.map +0 -1
  107. package/dist/chunk-UAKDG62Z.js.map +0 -1
  108. package/dist/chunk-WSQKHXW7.cjs.map +0 -1
  109. package/dist/chunk-X4N4WUKB.cjs.map +0 -1
  110. package/dist/types-DX4Mh4SL.d.cts +0 -217
  111. package/dist/types-DX4Mh4SL.d.ts +0 -217
  112. /package/dist/{chunk-NAXVVS5I.js.map → chunk-QJSOZFCZ.js.map} +0 -0
  113. /package/dist/{chunk-QGYDWPRM.js.map → chunk-WON34HKM.js.map} +0 -0
@@ -31,7 +31,7 @@ import {
31
31
  toBoolean,
32
32
  toText,
33
33
  tokenizeFormula
34
- } from "./chunk-FH6F67DN.js";
34
+ } from "./chunk-ZRNHGAQT.js";
35
35
 
36
36
  // src/spreadsheet/formula/dependency-graph.ts
37
37
  function extractNameDependencies(node) {
@@ -105,11 +105,178 @@ function extractCellDependencies(node) {
105
105
  visit(node);
106
106
  return deps;
107
107
  }
108
+ function extractReferenceDependencies(node) {
109
+ const deps = [];
110
+ const stack = [node];
111
+ while (stack.length > 0) {
112
+ const curr = stack.pop();
113
+ const sheet = "sheet" in curr && curr.sheet !== void 0 ? "sheetEnd" in curr && curr.sheetEnd !== void 0 ? { sheet: curr.sheet, sheetEnd: curr.sheetEnd } : { sheet: curr.sheet } : {};
114
+ switch (curr.type) {
115
+ case "CellRef":
116
+ case "SpillRef":
117
+ deps.push({ kind: "cell", col: curr.col.toUpperCase(), row: curr.row, ...sheet });
118
+ break;
119
+ case "Range": {
120
+ const startC = colNameToIndex(curr.startCol);
121
+ const endC = colNameToIndex(curr.endCol);
122
+ const whole = curr.endRow >= WHOLE_COLUMN_END_ROW;
123
+ deps.push({
124
+ kind: "range",
125
+ startCol: indexToColName(Math.min(startC, endC)),
126
+ endCol: indexToColName(Math.max(startC, endC)),
127
+ startRow: whole ? 1 : Math.min(curr.startRow, curr.endRow),
128
+ endRow: whole ? WHOLE_COLUMN_END_ROW : Math.max(curr.startRow, curr.endRow),
129
+ ...sheet
130
+ });
131
+ break;
132
+ }
133
+ case "BinaryOp":
134
+ stack.push(curr.right, curr.left);
135
+ break;
136
+ case "UnaryOp":
137
+ stack.push(curr.argument);
138
+ break;
139
+ case "FunctionCall":
140
+ for (let i = curr.args.length - 1; i >= 0; i--) stack.push(curr.args[i]);
141
+ break;
142
+ case "Invoke":
143
+ for (let i = curr.args.length - 1; i >= 0; i--) stack.push(curr.args[i]);
144
+ stack.push(curr.callee);
145
+ break;
146
+ default:
147
+ break;
148
+ }
149
+ }
150
+ return deps;
151
+ }
152
+ function rangeDependencyKey(startCol, startRow, endCol, endRow, sheetPrefix = "") {
153
+ const prefix = sheetPrefix === "" ? "" : `${sheetPrefix}!`;
154
+ return `${prefix}${startCol}${startRow}:${endCol}${endRow}`.toUpperCase();
155
+ }
156
+ var WIDE_RANGE_COLUMNS = 64;
157
+ var PENDING_INTERVALS = 32;
158
+ var IntervalIndex = class {
159
+ items = [];
160
+ dead = new Uint8Array(0);
161
+ deadCount = 0;
162
+ maxEnd = new Float64Array(0);
163
+ leaves = 0;
164
+ position = /* @__PURE__ */ new Map();
165
+ pending = /* @__PURE__ */ new Map();
166
+ /** Live entries. */
167
+ get size() {
168
+ return this.items.length - this.deadCount + this.pending.size;
169
+ }
170
+ /** Adds (or revives) the entry `id`. An id always names the same interval. */
171
+ insert(r0, r1, id, payload) {
172
+ const at = this.position.get(id);
173
+ if (at !== void 0) {
174
+ if (this.dead[at]) {
175
+ this.dead[at] = 0;
176
+ this.deadCount--;
177
+ this.items[at].payload = payload;
178
+ }
179
+ return;
180
+ }
181
+ this.pending.set(id, { r0, r1, id, payload });
182
+ }
183
+ remove(id) {
184
+ const at = this.position.get(id);
185
+ if (at !== void 0) {
186
+ if (!this.dead[at]) {
187
+ this.dead[at] = 1;
188
+ this.deadCount++;
189
+ }
190
+ return;
191
+ }
192
+ this.pending.delete(id);
193
+ }
194
+ /** Merges pending insertions and drops dead entries when there are enough of them. */
195
+ settle() {
196
+ if (this.pending.size <= PENDING_INTERVALS && this.deadCount <= Math.max(this.items.length / 2, PENDING_INTERVALS)) return;
197
+ const all = [];
198
+ this.items.forEach((item, i) => {
199
+ if (!this.dead[i]) all.push(item);
200
+ });
201
+ for (const item of this.pending.values()) all.push(item);
202
+ all.sort((a, b) => a.r0 - b.r0 || a.r1 - b.r1 || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
203
+ let leaves = 1;
204
+ while (leaves < all.length) leaves *= 2;
205
+ const maxEnd = new Float64Array(2 * leaves).fill(-1);
206
+ all.forEach((item, i) => {
207
+ maxEnd[leaves + i] = item.r1;
208
+ });
209
+ for (let i = leaves - 1; i >= 1; i--) maxEnd[i] = Math.max(maxEnd[2 * i], maxEnd[2 * i + 1]);
210
+ this.items = all;
211
+ this.dead = new Uint8Array(all.length);
212
+ this.deadCount = 0;
213
+ this.maxEnd = maxEnd;
214
+ this.leaves = leaves;
215
+ this.position = new Map(all.map((item, i) => [item.id, i]));
216
+ this.pending = /* @__PURE__ */ new Map();
217
+ }
218
+ /** Calls `fn` for every live interval overlapping rows `a..b` (`r0 <= b && r1 >= a`). */
219
+ query(a, b, fn) {
220
+ this.settle();
221
+ for (const item of this.pending.values()) {
222
+ if (item.r0 <= b && item.r1 >= a) fn(item.payload, item);
223
+ }
224
+ let lo = 0;
225
+ let hi = this.items.length;
226
+ while (lo < hi) {
227
+ const mid = lo + hi >>> 1;
228
+ if (this.items[mid].r0 <= b) lo = mid + 1;
229
+ else hi = mid;
230
+ }
231
+ if (lo === 0) return;
232
+ this.walk(1, 0, this.leaves, lo, a, fn);
233
+ }
234
+ walk(node, lo, hi, p, a, fn) {
235
+ if (lo >= p || this.maxEnd[node] < a) return;
236
+ if (hi - lo <= 1) {
237
+ const item = this.items[lo];
238
+ if (item && !this.dead[lo] && item.r1 >= a) fn(item.payload, item);
239
+ return;
240
+ }
241
+ const mid = lo + (hi - lo >>> 1);
242
+ this.walk(2 * node, lo, mid, p, a, fn);
243
+ this.walk(2 * node + 1, mid, hi, p, a, fn);
244
+ }
245
+ };
246
+ var CELL_KEY = /^([A-Z]+)(\d+)$/;
247
+ var RANGE_KEY = /^([A-Z]+)(\d+):([A-Z]+)(\d+)$/;
248
+ var COLUMN_KEY = /^([A-Z]+):$/;
249
+ function parseKey(key) {
250
+ const bang = key.lastIndexOf("!");
251
+ const sheet = bang < 0 ? "" : key.slice(0, bang);
252
+ const ref = bang < 0 ? key : key.slice(bang + 1);
253
+ let m = CELL_KEY.exec(ref);
254
+ if (m) return { kind: "cell", sheet, col: colNameToIndex(m[1]), row: Number(m[2]) };
255
+ m = RANGE_KEY.exec(ref);
256
+ if (m) {
257
+ const ca = colNameToIndex(m[1]);
258
+ const cb = colNameToIndex(m[3]);
259
+ const ra = Number(m[2]);
260
+ const rb = Number(m[4]);
261
+ return { kind: "range", sheet, c0: Math.min(ca, cb), c1: Math.max(ca, cb), r0: Math.min(ra, rb), r1: Math.max(ra, rb) };
262
+ }
263
+ m = COLUMN_KEY.exec(ref);
264
+ if (m) {
265
+ const c = colNameToIndex(m[1]);
266
+ return { kind: "range", sheet, c0: c, c1: c, r0: 1, r1: WHOLE_COLUMN_END_ROW };
267
+ }
268
+ return { kind: "opaque" };
269
+ }
270
+ var VIRTUAL_PREFIX = "\0";
108
271
  var DependencyGraph = class {
109
- // Map of cellKey (e.g. "B2") -> Set of cellKeys that this cell depends ON
110
- dependencies = /* @__PURE__ */ new Map();
111
- // Map of cellKey (e.g. "A1") -> Set of cellKeys that depend ON this cell (dependents)
112
- dependents = /* @__PURE__ */ new Map();
272
+ // Formula cell -> what it reads (single cells and whole ranges).
273
+ nodes = /* @__PURE__ */ new Map();
274
+ // Cell -> formulas reading it as a single cell.
275
+ cellDependents = /* @__PURE__ */ new Map();
276
+ // Sheet ('' for unqualified keys) -> interval index of the ranges formulas read.
277
+ rangeIndex = /* @__PURE__ */ new Map();
278
+ // Formulas recalculated on every recalculation (NOW, RAND, …).
279
+ volatile = /* @__PURE__ */ new Set();
113
280
  // Dynamic-array edges, set by the controller after evaluating an anchor:
114
281
  // spilled cells depend on their anchor, and a blocked (#SPILL!) anchor
115
282
  // depends on the cells blocking it. Kept apart from formula dependencies,
@@ -118,60 +285,145 @@ var DependencyGraph = class {
118
285
  spillDependents = /* @__PURE__ */ new Map();
119
286
  // anchor -> the edges it registered (so they can be replaced).
120
287
  spillEdges = /* @__PURE__ */ new Map();
288
+ // Blocked anchor -> its blockers. These edges only propagate changes
289
+ // (clearing a blocker recalculates the anchor): whether a cell blocks
290
+ // depends on its input, not its value, so they don't order evaluation and
291
+ // can't close a cycle.
292
+ blockingEdges = /* @__PURE__ */ new Map();
293
+ parsed = /* @__PURE__ */ new Map();
294
+ parse(key) {
295
+ let parsed = this.parsed.get(key);
296
+ if (!parsed) {
297
+ parsed = parseKey(key);
298
+ if (this.parsed.size > 2e5) this.parsed.clear();
299
+ this.parsed.set(key, parsed);
300
+ }
301
+ return parsed;
302
+ }
303
+ sheetIndex(sheet) {
304
+ let index = this.rangeIndex.get(sheet);
305
+ if (!index) {
306
+ index = { cols: /* @__PURE__ */ new Map(), wide: new IntervalIndex() };
307
+ this.rangeIndex.set(sheet, index);
308
+ }
309
+ return index;
310
+ }
311
+ removeNode(key) {
312
+ const node = this.nodes.get(key);
313
+ if (!node) return;
314
+ for (const dep of node.cells) {
315
+ const set = this.cellDependents.get(dep);
316
+ if (!set) continue;
317
+ set.delete(key);
318
+ if (set.size === 0) this.cellDependents.delete(dep);
319
+ }
320
+ for (const range of node.ranges) {
321
+ const index = this.rangeIndex.get(range.sheet);
322
+ if (!index) continue;
323
+ if (range.c1 - range.c0 + 1 > WIDE_RANGE_COLUMNS) {
324
+ index.wide.remove(`${range.r0}:${range.r1}:${range.c0}:${range.c1}:${key}`);
325
+ } else {
326
+ for (let c = range.c0; c <= range.c1; c++) index.cols.get(c)?.remove(`${range.r0}:${range.r1}:${key}`);
327
+ }
328
+ }
329
+ this.nodes.delete(key);
330
+ }
121
331
  /**
122
- * Sets the cells `cellKey` depends on directly (keys such as `B2`, or a
123
- * column key from `columnDependencyKey`), replacing its previous
124
- * dependencies. Used by spreadsheets whose keys carry more than A1 text
125
- * (e.g. sheet-qualified keys); `setCellFormula` derives them from a formula.
332
+ * Sets what `cellKey` depends on directly, replacing its previous
333
+ * dependencies. Keys are single cells (`B2`), rectangles (`A1:B9`) or
334
+ * whole columns (`A:`, see `columnDependencyKey`), each optionally
335
+ * sheet-qualified (`SHEET!A1:A9`). Ranges are stored whole in an interval
336
+ * index, never expanded cell by cell. `setCellFormula` derives the keys
337
+ * from a formula.
126
338
  */
127
339
  setCellDependencies(cellKey2, dependencyKeys) {
128
340
  const key = cellKey2.toUpperCase();
129
- for (const dep of this.dependencies.get(key) ?? []) {
130
- this.dependents.get(dep)?.delete(key);
131
- }
132
- this.dependencies.delete(key);
133
- const deps = /* @__PURE__ */ new Set();
341
+ this.removeNode(key);
342
+ const node = { cells: /* @__PURE__ */ new Set(), ranges: [] };
343
+ const seenRanges = /* @__PURE__ */ new Set();
134
344
  for (const raw of dependencyKeys) {
135
345
  const dep = raw.toUpperCase();
136
- deps.add(dep);
137
- if (!this.dependents.has(dep)) this.dependents.set(dep, /* @__PURE__ */ new Set());
138
- this.dependents.get(dep).add(key);
346
+ const parsed = this.parse(dep);
347
+ if (parsed.kind === "range" && !(parsed.c0 === parsed.c1 && parsed.r0 === parsed.r1)) {
348
+ if (seenRanges.has(dep)) continue;
349
+ seenRanges.add(dep);
350
+ node.ranges.push({ key: dep, sheet: parsed.sheet, c0: parsed.c0, c1: parsed.c1, r0: parsed.r0, r1: parsed.r1 });
351
+ continue;
352
+ }
353
+ const cell = parsed.kind === "range" ? `${parsed.sheet === "" ? "" : `${parsed.sheet}!`}${indexToColName(parsed.c0)}${parsed.r0}` : dep;
354
+ node.cells.add(cell);
355
+ }
356
+ if (node.cells.size === 0 && node.ranges.length === 0) {
357
+ this.volatile.delete(key);
358
+ return;
139
359
  }
140
- if (deps.size > 0) this.dependencies.set(key, deps);
360
+ for (const dep of node.cells) {
361
+ let set = this.cellDependents.get(dep);
362
+ if (!set) this.cellDependents.set(dep, set = /* @__PURE__ */ new Set());
363
+ set.add(key);
364
+ }
365
+ for (const range of node.ranges) {
366
+ const index = this.sheetIndex(range.sheet);
367
+ if (range.c1 - range.c0 + 1 > WIDE_RANGE_COLUMNS) {
368
+ index.wide.insert(range.r0, range.r1, `${range.r0}:${range.r1}:${range.c0}:${range.c1}:${key}`, {
369
+ c0: range.c0,
370
+ c1: range.c1,
371
+ key
372
+ });
373
+ } else {
374
+ for (let c = range.c0; c <= range.c1; c++) {
375
+ let col = index.cols.get(c);
376
+ if (!col) index.cols.set(c, col = new IntervalIndex());
377
+ col.insert(range.r0, range.r1, `${range.r0}:${range.r1}:${key}`, key);
378
+ }
379
+ }
380
+ }
381
+ this.nodes.set(key, node);
382
+ }
383
+ /**
384
+ * Marks `cellKey` as volatile (its formula uses NOW, RAND, …): it is
385
+ * recalculated by every plan made with `includeVolatile`, whatever changed.
386
+ */
387
+ setVolatile(cellKey2, volatile) {
388
+ const key = cellKey2.toUpperCase();
389
+ if (volatile) this.volatile.add(key);
390
+ else this.volatile.delete(key);
391
+ }
392
+ /** Cells whose formulas are volatile. */
393
+ getVolatileKeys() {
394
+ return Array.from(this.volatile);
141
395
  }
142
396
  setCellFormula(cellKey2, formulaStr) {
143
397
  const key = cellKey2.toUpperCase();
144
- const oldDeps = this.dependencies.get(key) || /* @__PURE__ */ new Set();
145
- for (const dep of oldDeps) {
146
- this.dependents.get(dep)?.delete(key);
147
- }
148
- this.dependencies.delete(key);
149
398
  if (!formulaStr || !formulaStr.startsWith("=")) {
399
+ this.setCellDependencies(key, []);
150
400
  return;
151
401
  }
402
+ let ast;
152
403
  try {
153
- const ast = parseFormula(formulaStr);
154
- const deps = extractCellDependencies(ast);
155
- const newDeps = /* @__PURE__ */ new Set();
156
- for (const d of deps) {
157
- const depKey = d.row === 0 ? columnDependencyKey(d.col) : `${d.col}${d.row}`.toUpperCase();
158
- newDeps.add(depKey);
159
- if (!this.dependents.has(depKey)) {
160
- this.dependents.set(depKey, /* @__PURE__ */ new Set());
161
- }
162
- this.dependents.get(depKey).add(key);
163
- }
164
- this.dependencies.set(key, newDeps);
404
+ ast = parseFormula(formulaStr);
165
405
  } catch {
406
+ this.setCellDependencies(key, []);
407
+ return;
408
+ }
409
+ const keys = [];
410
+ for (const dep of extractReferenceDependencies(ast)) {
411
+ keys.push(
412
+ dep.kind === "cell" ? `${dep.col}${dep.row}` : dep.endRow >= WHOLE_COLUMN_END_ROW && dep.startCol === dep.endCol ? columnDependencyKey(dep.startCol) : rangeDependencyKey(dep.startCol, dep.startRow, dep.endCol, dep.endRow)
413
+ );
166
414
  }
415
+ this.setCellDependencies(key, keys);
167
416
  }
168
417
  /** Removes every registered formula and dependency. */
169
418
  clear() {
170
- this.dependencies.clear();
171
- this.dependents.clear();
419
+ this.nodes.clear();
420
+ this.cellDependents.clear();
421
+ this.rangeIndex.clear();
422
+ this.volatile.clear();
172
423
  this.spillDependencies.clear();
173
424
  this.spillDependents.clear();
174
425
  this.spillEdges.clear();
426
+ this.blockingEdges.clear();
175
427
  }
176
428
  /**
177
429
  * Records the dynamic-array edges of the anchor `anchorKey`, replacing its
@@ -186,9 +438,15 @@ var DependencyGraph = class {
186
438
  this.spillDependencies.get(from)?.delete(to);
187
439
  this.spillDependents.get(to)?.delete(from);
188
440
  }
441
+ this.blockingEdges.delete(anchor);
189
442
  const edges = [];
190
443
  for (const key of spilledKeys) edges.push([key.toUpperCase(), anchor]);
191
- for (const key of blockingKeys) edges.push([anchor, key.toUpperCase()]);
444
+ const blockers = /* @__PURE__ */ new Set();
445
+ for (const key of blockingKeys) {
446
+ edges.push([anchor, key.toUpperCase()]);
447
+ blockers.add(key.toUpperCase());
448
+ }
449
+ if (blockers.size > 0) this.blockingEdges.set(anchor, blockers);
192
450
  for (const [from, to] of edges) {
193
451
  if (!this.spillDependencies.has(from)) this.spillDependencies.set(from, /* @__PURE__ */ new Set());
194
452
  this.spillDependencies.get(from).add(to);
@@ -198,133 +456,201 @@ var DependencyGraph = class {
198
456
  if (edges.length > 0) this.spillEdges.set(anchor, edges);
199
457
  else this.spillEdges.delete(anchor);
200
458
  }
459
+ /**
460
+ * Drops the edge from a spilled cell to the anchor whose values it holds,
461
+ * for a cell that just got input of its own: its value no longer comes
462
+ * from the anchor (which recalculates and becomes `#SPILL!`), and the
463
+ * stale edge could otherwise close a false cycle with the cell's new
464
+ * formula before the anchor is evaluated again.
465
+ */
466
+ detachSpilledCell(cellKey2) {
467
+ const key = cellKey2.toUpperCase();
468
+ const anchors = this.spillDependencies.get(key);
469
+ if (!anchors) return;
470
+ for (const anchor of Array.from(anchors)) {
471
+ if (this.blockingEdges.get(key)?.has(anchor)) continue;
472
+ const edges = this.spillEdges.get(anchor);
473
+ if (!edges?.some(([from, to]) => from === key && to === anchor)) continue;
474
+ anchors.delete(anchor);
475
+ this.spillDependents.get(anchor)?.delete(key);
476
+ const kept = edges.filter(([from, to]) => !(from === key && to === anchor));
477
+ if (kept.length > 0) this.spillEdges.set(anchor, kept);
478
+ else this.spillEdges.delete(anchor);
479
+ }
480
+ }
201
481
  /** The spilled cells registered for `anchorKey` by `setSpillEdges`. */
202
482
  getSpilledKeys(anchorKey) {
203
483
  const anchor = anchorKey.toUpperCase();
204
484
  return (this.spillEdges.get(anchor) ?? []).filter(([, to]) => to === anchor).map(([from]) => from);
205
485
  }
206
- /** Formula dependencies plus dynamic-array (spill) dependencies of a cell. */
486
+ /**
487
+ * What a cell depends on directly: single cells, ranges (as registered,
488
+ * e.g. `A1:A9` or `A:`, not expanded) and dynamic-array (spill) edges.
489
+ */
207
490
  getDependencies(cellKey2) {
208
491
  const key = cellKey2.toUpperCase();
209
- return unionOf(this.dependencies.get(key), this.spillDependencies.get(key));
492
+ const node = this.nodes.get(key);
493
+ const own = node ? [...node.cells, ...node.ranges.map((r) => r.key)] : [];
494
+ const spill = this.spillDependencies.get(key);
495
+ if (!spill || spill.size === 0) return own;
496
+ return Array.from(/* @__PURE__ */ new Set([...own, ...spill]));
497
+ }
498
+ /**
499
+ * Whether the formula in `formulaKey` reads `cellKey` directly: as a cell,
500
+ * inside one of its ranges, or through a spill edge.
501
+ */
502
+ dependsOn(formulaKey, cellKey2) {
503
+ const key = formulaKey.toUpperCase();
504
+ const target = cellKey2.toUpperCase();
505
+ if (this.spillDependencies.get(key)?.has(target)) return true;
506
+ const node = this.nodes.get(key);
507
+ if (!node) return false;
508
+ if (node.cells.has(target)) return true;
509
+ const parsed = this.parse(target);
510
+ if (parsed.kind !== "cell") return false;
511
+ return node.ranges.some(
512
+ (r) => r.sheet === parsed.sheet && parsed.col >= r.c0 && parsed.col <= r.c1 && parsed.row >= r.r0 && parsed.row <= r.r1
513
+ );
210
514
  }
211
- /** Cells depending on `cellKey`, through formulas or spills. */
515
+ /**
516
+ * Formulas (and spilled cells) reading `cellKey` directly, through formulas
517
+ * or spills. Found in O(log n + k) through the range index.
518
+ */
212
519
  getDependents(cellKey2) {
213
- const key = cellKey2.toUpperCase();
214
- return unionOf(this.dependents.get(key), this.spillDependents.get(key));
520
+ const out = /* @__PURE__ */ new Set();
521
+ this.collectDependents(cellKey2.toUpperCase(), (key) => out.add(key));
522
+ return Array.from(out);
523
+ }
524
+ collectDependents(key, add) {
525
+ const cells = this.cellDependents.get(key);
526
+ if (cells) for (const dep of cells) add(dep);
527
+ const spill = this.spillDependents.get(key);
528
+ if (spill) for (const dep of spill) add(dep);
529
+ if (this.rangeIndex.size === 0) return;
530
+ const parsed = this.parse(key);
531
+ if (parsed.kind !== "cell") return;
532
+ const index = this.rangeIndex.get(parsed.sheet);
533
+ if (!index) return;
534
+ index.cols.get(parsed.col)?.query(parsed.row, parsed.row, add);
535
+ if (index.wide.size > 0) {
536
+ index.wide.query(parsed.row, parsed.row, (wide) => {
537
+ if (parsed.col >= wide.c0 && parsed.col <= wide.c1) add(wide.key);
538
+ });
539
+ }
540
+ }
541
+ /** Sizes of the index: ranges count once (per column), not once per cell. */
542
+ getIndexStats() {
543
+ let cellEdges = 0;
544
+ let rangeEntries = 0;
545
+ for (const node of this.nodes.values()) {
546
+ cellEdges += node.cells.size;
547
+ for (const r of node.ranges) rangeEntries += r.c1 - r.c0 + 1 > WIDE_RANGE_COLUMNS ? 1 : r.c1 - r.c0 + 1;
548
+ }
549
+ return { formulas: this.nodes.size, cellEdges, rangeEntries, volatile: this.volatile.size };
215
550
  }
216
551
  /**
217
552
  * Detects if there is a cycle containing cellKey.
218
553
  */
219
554
  hasCycle(cellKey2) {
220
555
  const key = cellKey2.toUpperCase();
221
- const visited = /* @__PURE__ */ new Set();
222
- const recStack = /* @__PURE__ */ new Set();
223
- const dfs = (node) => {
224
- visited.add(node);
225
- recStack.add(node);
226
- const deps = this.getDependencies(node);
227
- for (const neighbor of deps) {
228
- if (!visited.has(neighbor)) {
229
- if (dfs(neighbor)) return true;
230
- } else if (recStack.has(neighbor)) {
231
- return true;
232
- }
233
- }
234
- recStack.delete(node);
235
- return false;
236
- };
237
- return dfs(key);
556
+ return this.getEvaluationPlan([key], { includeDependents: true }).cyclic.has(key);
238
557
  }
239
558
  /**
240
559
  * Returns cells affected by updating cellKey in topological order.
241
560
  */
242
561
  getEvaluationOrder(startCellKey) {
243
- const start = startCellKey.toUpperCase();
244
- const visited = /* @__PURE__ */ new Set();
245
- const recStack = /* @__PURE__ */ new Set();
246
- const order = [];
247
- let cycleDetected = false;
248
- const affected = /* @__PURE__ */ new Set();
249
- const collectAffected = (node) => {
250
- affected.add(node);
251
- const directDependents = this.dependents.get(node) || /* @__PURE__ */ new Set();
252
- for (const dep of directDependents) {
253
- if (!affected.has(dep)) {
254
- collectAffected(dep);
255
- }
256
- }
257
- };
258
- collectAffected(start);
259
- const visit = (node) => {
260
- if (recStack.has(node)) {
261
- cycleDetected = true;
262
- return;
263
- }
264
- if (visited.has(node)) return;
265
- recStack.add(node);
266
- visited.add(node);
267
- const deps = this.dependencies.get(node) || /* @__PURE__ */ new Set();
268
- for (const neighbor of deps) {
269
- if (affected.has(neighbor)) {
270
- visit(neighbor);
271
- }
272
- }
273
- recStack.delete(node);
274
- order.push(node);
275
- };
276
- for (const node of affected) {
277
- if (!visited.has(node)) {
278
- visit(node);
279
- }
280
- }
281
- return { order, hasCycle: cycleDetected };
562
+ const plan = this.getEvaluationPlan([startCellKey], { includeDependents: true });
563
+ return { order: plan.order, hasCycle: plan.cyclic.size > 0 };
282
564
  }
283
565
  /**
284
566
  * Plans a recalculation over `cellKeys` (and, with `includeDependents`, every
285
- * cell that transitively depends on them). Returns the keys in evaluation
567
+ * cell that transitively depends on them; with `includeVolatile`, every
568
+ * volatile formula and its dependents too). Returns the keys in evaluation
286
569
  * order (dependencies first) and the set of keys that are part of a cycle,
287
570
  * which must be marked `#CYCLE!` instead of evaluated. Every member of a
288
571
  * cycle is reported, not just the cell that closed it.
572
+ *
573
+ * Only the planned cells are ordered: a range a formula reads becomes one
574
+ * node whose edges are the planned cells inside it (found by binary
575
+ * search), so a range shared by many formulas is resolved once.
289
576
  */
290
577
  getEvaluationPlan(cellKeys, options = {}) {
291
578
  const nodes = /* @__PURE__ */ new Set();
292
579
  const queue = [];
293
- for (const key of cellKeys) {
294
- const upper = key.toUpperCase();
295
- if (!nodes.has(upper)) {
296
- nodes.add(upper);
297
- queue.push(upper);
580
+ const visit = (key) => {
581
+ if (!nodes.has(key)) {
582
+ nodes.add(key);
583
+ queue.push(key);
298
584
  }
299
- }
585
+ };
586
+ for (const key of cellKeys) visit(key.toUpperCase());
587
+ if (options.includeVolatile) for (const key of this.volatile) visit(key);
300
588
  if (options.includeDependents) {
301
- while (queue.length > 0) {
302
- const node = queue.pop();
303
- const direct = [
304
- ...this.dependents.get(node) ?? [],
305
- ...this.spillDependents.get(node) ?? [],
306
- ...this.dependents.get(columnDependencyKey(node.replace(/\d+$/, ""))) ?? []
307
- ];
308
- for (const dep of direct) {
309
- if (!nodes.has(dep)) {
310
- nodes.add(dep);
311
- queue.push(dep);
589
+ while (queue.length > 0) this.collectDependents(queue.pop(), visit);
590
+ }
591
+ let positions = null;
592
+ const planPositions = () => {
593
+ if (positions) return positions;
594
+ positions = /* @__PURE__ */ new Map();
595
+ for (const key of nodes) {
596
+ const parsed = this.parse(key);
597
+ if (parsed.kind !== "cell") continue;
598
+ let sheet = positions.get(parsed.sheet);
599
+ if (!sheet) positions.set(parsed.sheet, sheet = /* @__PURE__ */ new Map());
600
+ let cells = sheet.get(parsed.col);
601
+ if (!cells) sheet.set(parsed.col, cells = []);
602
+ cells.push([parsed.row, key]);
603
+ }
604
+ for (const sheet of positions.values()) for (const cells of sheet.values()) cells.sort((a, b) => a[0] - b[0]);
605
+ return positions;
606
+ };
607
+ const rangeMembers = /* @__PURE__ */ new Map();
608
+ const rangeNode = (range) => {
609
+ const id = `${VIRTUAL_PREFIX}${range.sheet}!${range.c0}:${range.c1}:${range.r0}:${range.r1}`;
610
+ let members = rangeMembers.get(id);
611
+ if (!members) {
612
+ const found = [];
613
+ const sheet = planPositions().get(range.sheet);
614
+ const scan = (cells) => {
615
+ let lo = 0;
616
+ let hi = cells.length;
617
+ while (lo < hi) {
618
+ const mid = lo + hi >>> 1;
619
+ if (cells[mid][0] < range.r0) lo = mid + 1;
620
+ else hi = mid;
621
+ }
622
+ for (let i = lo; i < cells.length && cells[i][0] <= range.r1; i++) found.push(cells[i][1]);
623
+ };
624
+ if (sheet) {
625
+ if (range.c1 - range.c0 + 1 <= sheet.size) {
626
+ for (let c = range.c0; c <= range.c1; c++) {
627
+ const cells = sheet.get(c);
628
+ if (cells) scan(cells);
629
+ }
630
+ } else {
631
+ for (const [c, cells] of sheet) if (c >= range.c0 && c <= range.c1) scan(cells);
312
632
  }
313
633
  }
634
+ rangeMembers.set(id, members = found);
314
635
  }
315
- }
636
+ return members.length > 0 ? id : null;
637
+ };
316
638
  const neighbors = (node) => {
639
+ if (node.startsWith(VIRTUAL_PREFIX)) return rangeMembers.get(node) ?? [];
317
640
  const result = [];
318
- for (const dep of this.getDependencies(node)) {
319
- if (dep.endsWith(":")) {
320
- const col = dep.slice(0, -1);
321
- for (const candidate of nodes) {
322
- if (candidate.replace(/\d+$/, "") === col) result.push(candidate);
323
- }
324
- } else if (nodes.has(dep)) {
325
- result.push(dep);
641
+ const formula = this.nodes.get(node);
642
+ if (formula) {
643
+ for (const dep of formula.cells) if (nodes.has(dep)) result.push(dep);
644
+ for (const range of formula.ranges) {
645
+ const id = rangeNode(range);
646
+ if (id) result.push(id);
326
647
  }
327
648
  }
649
+ const spill = this.spillDependencies.get(node);
650
+ if (spill) {
651
+ const blockers = this.blockingEdges.get(node);
652
+ for (const dep of spill) if (nodes.has(dep) && !blockers?.has(dep)) result.push(dep);
653
+ }
328
654
  return result;
329
655
  };
330
656
  const order = [];
@@ -372,6 +698,7 @@ var DependencyGraph = class {
372
698
  } while (member !== frame.node);
373
699
  const isCycle = component.length > 1 || frame.edges.includes(frame.node);
374
700
  for (const key of component) {
701
+ if (key.startsWith(VIRTUAL_PREFIX)) continue;
375
702
  order.push(key);
376
703
  if (isCycle) cyclic.add(key);
377
704
  }
@@ -381,11 +708,6 @@ var DependencyGraph = class {
381
708
  return { order, cyclic };
382
709
  }
383
710
  };
384
- function unionOf(a, b) {
385
- if (!b || b.size === 0) return Array.from(a ?? []);
386
- if (!a || a.size === 0) return Array.from(b);
387
- return Array.from(/* @__PURE__ */ new Set([...a, ...b]));
388
- }
389
711
 
390
712
  // src/spreadsheet/formula/references.ts
391
713
  var CELL_PART = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
@@ -2505,8 +2827,35 @@ var createRuntime = (doc) => ({
2505
2827
  });
2506
2828
  var sameName3 = (a, b) => a.toUpperCase() === b.toUpperCase();
2507
2829
  var DEFAULT_MAX_HISTORY_BYTES = 64 * 1024 * 1024;
2830
+ var AST_CACHE_SIZE = 1e5;
2831
+ var RANGE_CACHE_MIN_CELLS = 256;
2508
2832
  var MAX_SPILL_PASSES = 25;
2509
2833
  var spillCellId = (rowId, colId) => `${rowId}\0${colId}`;
2834
+ var PositionCache = class {
2835
+ map = /* @__PURE__ */ new Map();
2836
+ scanned = 0;
2837
+ find(list, id) {
2838
+ const pos = this.map.get(id);
2839
+ if (pos !== void 0 && list[pos]?.id === id) return pos;
2840
+ if (pos === void 0 && this.scanned <= list.length) {
2841
+ for (let i = this.scanned; i < list.length; i++) this.map.set(list[i].id, i);
2842
+ this.scanned = list.length;
2843
+ const found2 = this.map.get(id);
2844
+ if (found2 !== void 0 && list[found2]?.id === id) return found2;
2845
+ }
2846
+ this.map.clear();
2847
+ list.forEach((item, i) => this.map.set(item.id, i));
2848
+ this.scanned = list.length;
2849
+ const found = this.map.get(id);
2850
+ return found !== void 0 ? found : -1;
2851
+ }
2852
+ };
2853
+ var positionCaches = /* @__PURE__ */ new WeakMap();
2854
+ function positionOf(list, id) {
2855
+ let cache = positionCaches.get(list);
2856
+ if (!cache) positionCaches.set(list, cache = new PositionCache());
2857
+ return cache.find(list, id);
2858
+ }
2510
2859
  var SpreadsheetController = class {
2511
2860
  activeCell = null;
2512
2861
  selectedRange = null;
@@ -2546,6 +2895,10 @@ var SpreadsheetController = class {
2546
2895
  // runtimes, not in the documents, and every recalculation rebuilds them.
2547
2896
  // Cells (sheet-qualified keys) whose spill ownership changed during the current pass.
2548
2897
  spillChanges = null;
2898
+ recalcStats = { evaluated: 0, planned: 0, passes: 0 };
2899
+ // Values of large ranges read while running an evaluation plan (see runEvaluationPlan).
2900
+ astCache = /* @__PURE__ */ new Map();
2901
+ rangeCache = null;
2549
2902
  /** The active sheet's document (the sheet the editor shows). */
2550
2903
  get document() {
2551
2904
  return this.ctx.doc;
@@ -2617,7 +2970,7 @@ var SpreadsheetController = class {
2617
2970
  }
2618
2971
  this.evaluator = new FormulaEvaluator({
2619
2972
  resolveCell: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveCellByCoord(colStr, rowNum)),
2620
- resolveRange: (sCol, sRow, eCol, eRow, sheet) => this.onSheet(sheet, () => this.resolveRangeByCoords(sCol, sRow, eCol, eRow)),
2973
+ resolveRange: (sCol, sRow, eCol, eRow, sheet, options2) => this.onSheet(sheet, () => this.resolveRangeByCoords(sCol, sRow, eCol, eRow, options2)),
2621
2974
  resolveProperty: (prop) => this.resolveCurrentRowProperty(prop),
2622
2975
  resolveSpill: (colStr, rowNum, sheet) => this.onSheet(sheet, () => this.resolveSpillByCoord(colStr, rowNum)),
2623
2976
  resolveSheetSpan: (first3, last) => this.resolveSheetSpan(first3, last),
@@ -2877,6 +3230,8 @@ var SpreadsheetController = class {
2877
3230
  });
2878
3231
  }
2879
3232
  getColumn(colIdOrKey) {
3233
+ const byId = this.findColumn(colIdOrKey);
3234
+ if (byId) return byId;
2880
3235
  return this.document.columns.find(
2881
3236
  (c) => c.id === colIdOrKey || c.key.toUpperCase() === colIdOrKey.toUpperCase()
2882
3237
  );
@@ -2885,7 +3240,24 @@ var SpreadsheetController = class {
2885
3240
  if (typeof rowIdOrIndex === "number") {
2886
3241
  return this.document.rows[rowIdOrIndex];
2887
3242
  }
2888
- return this.document.rows.find((r) => r.id === rowIdOrIndex);
3243
+ return this.findRow(rowIdOrIndex);
3244
+ }
3245
+ /** The row with this id on the current sheet (constant time, see `PositionCache`). */
3246
+ findRow(rowId) {
3247
+ const pos = positionOf(this.document.rows, rowId);
3248
+ return pos < 0 ? void 0 : this.document.rows[pos];
3249
+ }
3250
+ /** The column with this id on the current sheet. */
3251
+ findColumn(colId) {
3252
+ const pos = positionOf(this.document.columns, colId);
3253
+ return pos < 0 ? void 0 : this.document.columns[pos];
3254
+ }
3255
+ /** The column whose key is `key` (`A`, `B`, …): keys follow column positions. */
3256
+ columnByKey(key) {
3257
+ const upper = key.toUpperCase();
3258
+ const col = this.document.columns[colNameToIndex(upper)];
3259
+ if (col && col.key.toUpperCase() === upper) return col;
3260
+ return this.document.columns.find((c) => c.key.toUpperCase() === upper);
2889
3261
  }
2890
3262
  getCell(rowId, colId) {
2891
3263
  const existing = this.document.cells[rowId]?.[colId];
@@ -2895,8 +3267,21 @@ var SpreadsheetController = class {
2895
3267
  if (spilled) return spilled;
2896
3268
  }
2897
3269
  if (existing && !existing.explicitNumberFormat) return existing;
2898
- const col = this.document.columns.find((c) => c.id === colId);
2899
- const row = this.document.rows.find((r) => r.id === rowId);
3270
+ return this.finishCell(existing, this.findRow(rowId), this.findColumn(colId));
3271
+ }
3272
+ /** `getCell` for a row and column already looked up. */
3273
+ cellOf(row, col) {
3274
+ const existing = this.document.cells[row.id]?.[col.id];
3275
+ if (existing && existing.raw !== "") return existing;
3276
+ if (this.spillCells.size > 0) {
3277
+ const spilled = this.spillCells.get(spillCellId(row.id, col.id));
3278
+ if (spilled) return spilled;
3279
+ }
3280
+ if (existing && !existing.explicitNumberFormat) return existing;
3281
+ return this.finishCell(existing, row, col);
3282
+ }
3283
+ /** A cell with no value of its own: derived (bound, column formula, blank), keeping an explicit number format. */
3284
+ finishCell(existing, row, col) {
2900
3285
  const derived = this.getDerivedCell(row, col);
2901
3286
  if (!existing) return derived;
2902
3287
  if (derived.raw === "") return existing;
@@ -2928,12 +3313,12 @@ var SpreadsheetController = class {
2928
3313
  return { raw: "", value: "", format: col?.format, ...this.fixedNumberFormatFields(col, void 0) };
2929
3314
  }
2930
3315
  setCellValue(rowId, colId, raw) {
2931
- this.recordSnapshot();
3316
+ this.recordCellSnapshot(rowId, colId);
2932
3317
  if (!this.document.cells[rowId]) {
2933
3318
  this.document.cells[rowId] = {};
2934
3319
  }
2935
- const col = this.document.columns.find((c) => c.id === colId);
2936
- const row = this.document.rows.find((r) => r.id === rowId);
3320
+ const col = this.findColumn(colId);
3321
+ const row = this.findRow(rowId);
2937
3322
  const isFormula = raw.startsWith("=");
2938
3323
  const existing = this.document.cells[rowId][colId];
2939
3324
  let cell;
@@ -2948,6 +3333,7 @@ var SpreadsheetController = class {
2948
3333
  }
2949
3334
  const cellKey2 = this.getCellKey(rowId, colId);
2950
3335
  this.registerFormula(cellKey2, isFormula ? raw : null);
3336
+ this.dropStaleSpillEdges(cellKey2, raw);
2951
3337
  this.document.cells[rowId][colId] = cell;
2952
3338
  this.document.updatedAt = Date.now();
2953
3339
  if (this.batching) {
@@ -2966,8 +3352,8 @@ var SpreadsheetController = class {
2966
3352
  this.onCellCommit?.(rowId, colId, raw, evaluatedVal);
2967
3353
  }
2968
3354
  getCellKey(rowId, colId) {
2969
- const row = this.document.rows.find((r) => r.id === rowId);
2970
- const col = this.document.columns.find((c) => c.id === colId);
3355
+ const row = this.findRow(rowId);
3356
+ const col = this.findColumn(colId);
2971
3357
  if (!row || !col) return `${colId}_${rowId}`;
2972
3358
  return `${col.key}${row.index + 1}`.toUpperCase();
2973
3359
  }
@@ -2990,22 +3376,57 @@ var SpreadsheetController = class {
2990
3376
  const key = this.qualify(cellKey2);
2991
3377
  if (!formula || !formula.startsWith("=")) {
2992
3378
  this.depGraph.setCellDependencies(key, []);
3379
+ this.depGraph.setVolatile(key, false);
2993
3380
  return;
2994
3381
  }
2995
3382
  let ast;
2996
3383
  try {
2997
- ast = parseFormula(formula);
3384
+ ast = this.parseCached(formula);
2998
3385
  } catch {
2999
3386
  this.depGraph.setCellDependencies(key, []);
3387
+ this.depGraph.setVolatile(key, false);
3000
3388
  return;
3001
3389
  }
3002
3390
  const deps = /* @__PURE__ */ new Set();
3003
- this.collectDependencies(ast, this.ctx, deps, /* @__PURE__ */ new Set());
3391
+ const volatile = { found: false };
3392
+ this.collectDependencies(ast, this.ctx, deps, /* @__PURE__ */ new Set(), volatile);
3004
3393
  this.depGraph.setCellDependencies(key, deps);
3394
+ this.depGraph.setVolatile(key, volatile.found);
3395
+ }
3396
+ /**
3397
+ * Spill edges made stale by new input in `cellKey`: the edges of the array
3398
+ * it anchored (its new formula re-spills when evaluated; spill changes
3399
+ * are recalculated in a further pass) and, when it now holds input, its
3400
+ * edge to the anchor whose values it held. Left in place they can close
3401
+ * false cycles with the new formulas before the anchors are evaluated.
3402
+ */
3403
+ dropStaleSpillEdges(cellKey2, raw) {
3404
+ const key = this.qualify(cellKey2);
3405
+ this.depGraph.setSpillEdges(key, []);
3406
+ if (raw !== "") this.depGraph.detachSpilledCell(key);
3407
+ }
3408
+ /**
3409
+ * The parsed formula, shared by dependency registration and evaluation (so
3410
+ * a load parses each formula once). ASTs are never mutated.
3411
+ */
3412
+ parseCached(formula) {
3413
+ let ast = this.astCache.get(formula);
3414
+ if (!ast) {
3415
+ ast = parseFormula(formula);
3416
+ if (this.astCache.size >= AST_CACHE_SIZE) this.astCache.clear();
3417
+ this.astCache.set(formula, ast);
3418
+ }
3419
+ return ast;
3005
3420
  }
3006
- /** Adds the qualified keys `ast` depends on, with unqualified references on `home`. */
3007
- collectDependencies(ast, home, out, seenNames) {
3008
- for (const dep of extractCellDependencies(ast)) {
3421
+ /**
3422
+ * Adds the qualified keys `ast` depends on, with unqualified references on
3423
+ * `home`: cells, and ranges kept whole (`SHEETID!A1:A50000`), which the
3424
+ * graph indexes as intervals instead of one key per cell. Sets
3425
+ * `volatile.found` when a volatile function (NOW, RAND, …) is used,
3426
+ * directly or through a defined name.
3427
+ */
3428
+ collectDependencies(ast, home, out, seenNames, volatile = { found: false }) {
3429
+ for (const dep of extractReferenceDependencies(ast)) {
3009
3430
  let sheets;
3010
3431
  if (dep.sheet === void 0) {
3011
3432
  sheets = [home];
@@ -3017,11 +3438,19 @@ var SpreadsheetController = class {
3017
3438
  sheets = Array.isArray(span) ? span.map((name) => this.findRuntimeByName(name)) : [];
3018
3439
  }
3019
3440
  for (const rt of sheets) {
3020
- out.add(dep.row === 0 ? columnDependencyKey(`${rt.doc.id}!${dep.col}`) : this.qualify(`${dep.col}${dep.row}`, rt));
3441
+ out.add(
3442
+ dep.kind === "cell" ? this.qualify(`${dep.col}${dep.row}`, rt) : rangeDependencyKey(dep.startCol, dep.startRow, dep.endCol, dep.endRow, rt.doc.id)
3443
+ );
3021
3444
  }
3022
3445
  }
3023
3446
  for (const used of extractNameDependencies(ast)) {
3024
- if (used.call && getFormulaFunction(used.name)) continue;
3447
+ if (used.call) {
3448
+ const fn = getFormulaFunction(used.name);
3449
+ if (fn) {
3450
+ if (fn.volatile) volatile.found = true;
3451
+ continue;
3452
+ }
3453
+ }
3025
3454
  const lookupSheet = used.sheet === void 0 ? home : this.findRuntimeByName(used.sheet);
3026
3455
  if (!lookupSheet) continue;
3027
3456
  const def = this.findName(used.name, lookupSheet);
@@ -3032,7 +3461,7 @@ var SpreadsheetController = class {
3032
3461
  if (seenNames.has(seenKey)) continue;
3033
3462
  seenNames.add(seenKey);
3034
3463
  try {
3035
- this.collectDependencies(parseFormula(def.refersTo), nameHome, out, seenNames);
3464
+ this.collectDependencies(parseFormula(def.refersTo), nameHome, out, seenNames, volatile);
3036
3465
  } catch {
3037
3466
  }
3038
3467
  }
@@ -3047,18 +3476,49 @@ var SpreadsheetController = class {
3047
3476
  recalculateKeys(keys, includeDependents) {
3048
3477
  let pending = Array.from(keys);
3049
3478
  let withDependents = includeDependents;
3050
- for (let pass = 0; pass < MAX_SPILL_PASSES && pending.length > 0; pass++) {
3479
+ let withVolatile = includeDependents;
3480
+ const stats = { evaluated: 0, planned: 0, passes: 0 };
3481
+ this.recalcStats = stats;
3482
+ for (let pass = 0; pass < MAX_SPILL_PASSES && (pending.length > 0 || withVolatile); pass++) {
3051
3483
  const changed = /* @__PURE__ */ new Set();
3052
3484
  this.spillChanges = changed;
3053
3485
  try {
3054
- this.runEvaluationPlan(this.depGraph.getEvaluationPlan(pending, { includeDependents: withDependents }));
3486
+ const plan = this.depGraph.getEvaluationPlan(pending, { includeDependents: withDependents, includeVolatile: withVolatile });
3487
+ stats.planned += plan.order.length;
3488
+ stats.passes++;
3489
+ this.runEvaluationPlan(plan);
3055
3490
  } finally {
3056
3491
  this.spillChanges = null;
3057
3492
  }
3058
3493
  pending = Array.from(changed);
3059
3494
  withDependents = true;
3495
+ withVolatile = false;
3060
3496
  }
3061
3497
  }
3498
+ /**
3499
+ * Recalculates cells of the current sheet whose stored input changed (their
3500
+ * formulas already registered), with their dependents, in one plan.
3501
+ */
3502
+ recalculateWrittenCells(cells) {
3503
+ const keys = /* @__PURE__ */ new Set();
3504
+ for (const [rowId, colId] of cells) {
3505
+ const cellKey2 = this.getCellKey(rowId, colId);
3506
+ keys.add(this.qualify(cellKey2));
3507
+ const anchor = this.spilledBy.get(cellKey2);
3508
+ if (anchor) keys.add(this.qualify(anchor));
3509
+ }
3510
+ this.recalculateKeys(keys, true);
3511
+ }
3512
+ /**
3513
+ * Work done by the last recalculation (an edit, paste, import, load or
3514
+ * full recalculation): formulas evaluated, cells planned (formulas, their
3515
+ * dependents and spilled cells) and passes (more than one when spill
3516
+ * ranges changed). Deterministic, unlike timings: use it to check that an
3517
+ * edit only recalculates what depends on it.
3518
+ */
3519
+ getRecalculationStats() {
3520
+ return { ...this.recalcStats };
3521
+ }
3062
3522
  /**
3063
3523
  * Rebuilds the dependency graph from scratch and recalculates every formula
3064
3524
  * in dependency order.
@@ -3072,10 +3532,14 @@ var SpreadsheetController = class {
3072
3532
  this.spills.clear();
3073
3533
  this.spilledBy.clear();
3074
3534
  this.spillCells.clear();
3535
+ const columns = this.document.columns;
3536
+ const deriving = columns.map((col) => col.type === "bound" || col.type === "formula");
3075
3537
  for (const row of this.document.rows) {
3076
- for (const col of this.document.columns) {
3077
- const cell = this.document.cells[row.id]?.[col.id] ?? this.getDerivedCell(row, col);
3078
- if (cell.raw.startsWith("=")) {
3538
+ const stored = this.document.cells[row.id];
3539
+ for (let c = 0; c < columns.length; c++) {
3540
+ const col = columns[c];
3541
+ const cell = stored?.[col.id] ?? (deriving[c] ? this.getDerivedCell(row, col) : null);
3542
+ if (cell && typeof cell.raw === "string" && cell.raw.startsWith("=")) {
3079
3543
  const cellKey2 = `${col.key}${row.index + 1}`.toUpperCase();
3080
3544
  this.registerFormula(cellKey2, cell.raw);
3081
3545
  formulaKeys.push(this.qualify(cellKey2));
@@ -3087,25 +3551,45 @@ var SpreadsheetController = class {
3087
3551
  this.recalculateKeys(formulaKeys, false);
3088
3552
  }
3089
3553
  runEvaluationPlan(plan) {
3090
- for (const qualified of plan.order) {
3091
- const located = this.unqualify(qualified);
3092
- if (!located) continue;
3093
- const onActive = located.rt === this.active;
3094
- this.withSheet(located.rt, () => {
3095
- if (onActive) this.markValueChanged(located.key);
3096
- if (plan.cyclic.has(qualified)) {
3097
- this.markCycle(located.key);
3098
- } else {
3099
- this.evalSingleCellByKey(located.key);
3100
- }
3101
- if (onActive) this.markValueChanged(located.key);
3102
- });
3554
+ this.rangeCache = /* @__PURE__ */ new Map();
3555
+ try {
3556
+ for (const qualified of plan.order) {
3557
+ const located = this.unqualify(qualified);
3558
+ if (!located) continue;
3559
+ const onActive = located.rt === this.active;
3560
+ this.withSheet(located.rt, () => {
3561
+ const hadSpill = this.spills.has(located.key);
3562
+ const spillChanges = this.spillChanges?.size ?? 0;
3563
+ if (onActive) this.markValueChanged(located.key);
3564
+ if (plan.cyclic.has(qualified)) {
3565
+ this.markCycle(located.key);
3566
+ } else {
3567
+ this.evalSingleCellByKey(located.key);
3568
+ }
3569
+ if (onActive) this.markValueChanged(located.key);
3570
+ const spilled = hadSpill || this.spills.has(located.key) || (this.spillChanges?.size ?? 0) !== spillChanges;
3571
+ this.invalidateRangeCache(located.rt.doc.id, spilled ? null : colNameToIndex(located.key.replace(/\d+$/, "")));
3572
+ });
3573
+ }
3574
+ } finally {
3575
+ this.rangeCache = null;
3576
+ }
3577
+ }
3578
+ /**
3579
+ * Drops cached ranges of `sheetId` that cover column `col` (whose cell was
3580
+ * just evaluated), or all of the sheet's when `col` is null (spilled
3581
+ * values moved or changed).
3582
+ */
3583
+ invalidateRangeCache(sheetId, col) {
3584
+ if (!this.rangeCache || this.rangeCache.size === 0) return;
3585
+ for (const [key, entry] of this.rangeCache) {
3586
+ if (entry.sheet === sheetId && (col === null || col >= entry.c0 && col <= entry.c1)) this.rangeCache.delete(key);
3103
3587
  }
3104
3588
  }
3105
3589
  locateCellKey(cellKey2) {
3106
3590
  const match = cellKey2.match(/^([A-Za-z]+)(\d+)$/);
3107
3591
  if (!match) return null;
3108
- const col = this.document.columns.find((c) => c.key.toUpperCase() === match[1].toUpperCase());
3592
+ const col = this.columnByKey(match[1]);
3109
3593
  const row = this.document.rows[parseInt(match[2], 10) - 1];
3110
3594
  return col && row ? { row, col } : null;
3111
3595
  }
@@ -3117,10 +3601,8 @@ var SpreadsheetController = class {
3117
3601
  if (!cell.raw.startsWith("=")) return;
3118
3602
  if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
3119
3603
  const qualified = this.qualify(cellKey2);
3120
- const deps = this.depGraph.getDependencies(qualified);
3121
- const selfReference = deps.includes(qualified) || deps.includes(columnDependencyKey(`${this.document.id}!${col.key}`));
3122
- const spilledKeys = this.depGraph.getSpilledKeys(qualified);
3123
- const ownSpill = deps.find((dep) => spilledKeys.includes(dep));
3604
+ const selfReference = this.depGraph.dependsOn(qualified, qualified);
3605
+ const ownSpill = selfReference ? void 0 : this.depGraph.getSpilledKeys(qualified).find((key) => this.depGraph.dependsOn(qualified, key));
3124
3606
  const errorMessage = selfReference ? "This formula can't reference its own cell." : ownSpill ? `This formula refers to ${ownSpill.slice(ownSpill.lastIndexOf("!") + 1)}, which holds its own spilled results (a circular reference).` : "This formula is part of a circular reference: it depends on its own result through other cells.";
3125
3607
  this.clearSpill(cellKey2.toUpperCase(), true);
3126
3608
  this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!", errorMessage };
@@ -3130,7 +3612,7 @@ var SpreadsheetController = class {
3130
3612
  if (!match) return;
3131
3613
  const colName = match[1].toUpperCase();
3132
3614
  const rowNum = parseInt(match[2], 10);
3133
- const col = this.document.columns.find((c) => c.key.toUpperCase() === colName);
3615
+ const col = this.columnByKey(colName);
3134
3616
  const row = this.document.rows[rowNum - 1];
3135
3617
  if (!col || !row) return;
3136
3618
  const cell = this.getCell(row.id, col.id);
@@ -3145,13 +3627,19 @@ var SpreadsheetController = class {
3145
3627
  try {
3146
3628
  this.evaluatingRowIndex = rowNum - 1;
3147
3629
  this.evaluatingCell = { col: colName, row: rowNum };
3148
- const result = this.evaluator.evaluateWithDiagnostics(cell.raw);
3630
+ this.recalcStats.evaluated++;
3631
+ let parsed = cell.raw;
3632
+ try {
3633
+ parsed = this.parseCached(cell.raw);
3634
+ } catch {
3635
+ }
3636
+ const result = this.evaluator.evaluateWithDiagnostics(parsed);
3149
3637
  if (!this.document.cells[row.id]) {
3150
3638
  this.document.cells[row.id] = {};
3151
3639
  }
3152
3640
  let { value, error, message } = result;
3153
3641
  if (result.array) {
3154
- const blocked = this.placeSpill(`${colName}${rowNum}`, rowNum - 1, this.document.columns.indexOf(col), result.array);
3642
+ const blocked = this.placeSpill(`${colName}${rowNum}`, rowNum - 1, positionOf(this.document.columns, col.id), result.array);
3155
3643
  if (blocked) {
3156
3644
  value = "#SPILL!";
3157
3645
  error = "#SPILL!";
@@ -3215,6 +3703,7 @@ var SpreadsheetController = class {
3215
3703
  return reason;
3216
3704
  }
3217
3705
  const previous = this.spills.get(anchor);
3706
+ const linked = new Set(this.depGraph.getSpilledKeys(this.qualify(anchor)));
3218
3707
  const cells = [];
3219
3708
  for (let c = 0; c < cols; c++) {
3220
3709
  const column = this.document.columns[col + c];
@@ -3238,6 +3727,8 @@ var SpreadsheetController = class {
3238
3727
  if (this.spilledBy.get(key) !== anchor) {
3239
3728
  this.spilledBy.set(key, anchor);
3240
3729
  this.spillChanges?.add(this.qualify(key));
3730
+ } else if (!linked.has(this.qualify(key))) {
3731
+ this.spillChanges?.add(this.qualify(key));
3241
3732
  }
3242
3733
  cells.push({ key, rowId: rowObj.id, colId: column.id });
3243
3734
  }
@@ -3317,36 +3808,65 @@ var SpreadsheetController = class {
3317
3808
  return false;
3318
3809
  }
3319
3810
  resolveCellByCoord(colStr, rowNum) {
3320
- const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
3811
+ const col = this.columnByKey(colStr);
3321
3812
  const row = this.document.rows[rowNum - 1];
3322
3813
  if (!col || !row) return 0;
3323
3814
  const override = this.valueOverride;
3324
3815
  if (override && override.row === rowNum - 1 && this.document.columns[override.col] === col) return override.value;
3325
- return this.getCell(row.id, col.id).value ?? "";
3816
+ return this.cellOf(row, col).value ?? "";
3326
3817
  }
3327
- resolveRangeByCoords(sCol, sRow, eCol, eRow) {
3818
+ /**
3819
+ * Values of a range, column by column. With `skipBlanks` (functions that
3820
+ * ignore blank cells: SUM, COUNT, AVERAGE, …) blank cells are left out, so
3821
+ * a sparse column costs one lookup per row and no array slot.
3822
+ */
3823
+ resolveRangeByCoords(sCol, sRow, eCol, eRow, options) {
3328
3824
  const startC = colNameToIndex(sCol);
3329
3825
  const endC = colNameToIndex(eCol);
3330
3826
  const minC = Math.min(startC, endC);
3331
3827
  const maxC = Math.max(startC, endC);
3332
3828
  const minR = Math.min(sRow, eRow);
3333
3829
  const maxR = Math.min(Math.max(sRow, eRow), WHOLE_COLUMN_END_ROW, this.document.rows.length);
3830
+ const skipBlanks = options?.skipBlanks === true;
3831
+ const cacheable = this.rangeCache !== null && !this.valueOverride && (maxC - minC + 1) * (maxR - minR + 1) >= RANGE_CACHE_MIN_CELLS;
3832
+ const cacheKey = cacheable ? `${this.document.id}|${minC}|${maxC}|${minR}|${maxR}|${skipBlanks ? 1 : 0}` : "";
3833
+ if (cacheable) {
3834
+ const hit = this.rangeCache.get(cacheKey);
3835
+ if (hit) return hit.values.slice();
3836
+ }
3837
+ const rows = this.document.rows;
3838
+ const cells = this.document.cells;
3839
+ const spills = this.spillCells.size > 0 ? this.spillCells : null;
3334
3840
  const results = [];
3335
3841
  for (let c = minC; c <= maxC; c++) {
3336
- const colName = indexToColName(c);
3337
- const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);
3842
+ const col = this.columnByKey(indexToColName(c));
3338
3843
  if (!col) continue;
3844
+ const plain = col.type !== "bound" && col.type !== "formula";
3845
+ const colId = col.id;
3339
3846
  const override = this.valueOverride && this.document.columns[this.valueOverride.col] === col ? this.valueOverride : null;
3340
3847
  for (let r = minR; r <= maxR; r++) {
3341
- const row = this.document.rows[r - 1];
3848
+ const row = rows[r - 1];
3342
3849
  if (!row) continue;
3850
+ let value;
3343
3851
  if (override && override.row === r - 1) {
3344
- results.push(override.value);
3345
- continue;
3852
+ value = override.value;
3853
+ } else {
3854
+ const stored = cells[row.id]?.[colId];
3855
+ if (stored && stored.raw !== "") value = stored.value;
3856
+ else {
3857
+ const spilled = spills?.get(spillCellId(row.id, colId));
3858
+ if (spilled) value = spilled.value;
3859
+ else if (stored && (plain || !stored.explicitNumberFormat)) value = stored.value;
3860
+ else if (plain) value = "";
3861
+ else value = this.cellOf(row, col).value;
3862
+ }
3346
3863
  }
3347
- results.push(this.getCell(row.id, col.id).value ?? "");
3864
+ if (value === void 0 || value === null) value = "";
3865
+ if (skipBlanks && value === "") continue;
3866
+ results.push(value);
3348
3867
  }
3349
3868
  }
3869
+ if (cacheable) this.rangeCache.set(cacheKey, { sheet: this.document.id, c0: minC, c1: maxC, values: results.slice() });
3350
3870
  return results;
3351
3871
  }
3352
3872
  resolveCurrentRowProperty(prop) {
@@ -3438,8 +3958,8 @@ var SpreadsheetController = class {
3438
3958
  */
3439
3959
  commitCellInput(rowId, colId, raw) {
3440
3960
  if (this.validationPrompt) return { status: "pending", prompt: this.validationPrompt };
3441
- const row = this.document.rows.findIndex((r) => r.id === rowId);
3442
- const col = this.document.columns.findIndex((c) => c.id === colId);
3961
+ const row = positionOf(this.document.rows, rowId);
3962
+ const col = positionOf(this.document.columns, colId);
3443
3963
  if (row === -1 || col === -1) return { status: "none" };
3444
3964
  const failed = this.checkInput(rowId, colId, row, col, raw, false);
3445
3965
  if (failed) return failed;
@@ -3708,6 +4228,8 @@ var SpreadsheetController = class {
3708
4228
  */
3709
4229
  runBatch(fn) {
3710
4230
  this.recordSnapshot();
4231
+ const rowCount = this.document.rows.length;
4232
+ const colCount = this.document.columns.length;
3711
4233
  this.batching = true;
3712
4234
  this.pendingCommits = [];
3713
4235
  try {
@@ -3715,8 +4237,14 @@ var SpreadsheetController = class {
3715
4237
  } finally {
3716
4238
  this.batching = false;
3717
4239
  }
4240
+ const grew = this.document.rows.length !== rowCount || this.document.columns.length !== colCount;
3718
4241
  this.recomputeRowIndices();
3719
- this.recalculateAll();
4242
+ if (grew) {
4243
+ this.recalculateAll();
4244
+ } else {
4245
+ this.cfDirty = "all";
4246
+ this.recalculateWrittenCells(this.pendingCommits.map(([rowId, colId]) => [rowId, colId]));
4247
+ }
3720
4248
  this.document.updatedAt = Date.now();
3721
4249
  this.notifyChange();
3722
4250
  const commits = this.pendingCommits;
@@ -4742,6 +5270,17 @@ var SpreadsheetController = class {
4742
5270
  if (this.isApplyingHistory || this.batching) return;
4743
5271
  this.pushUndo({ kind: "sheet", sheetId: this.ctx.doc.id, data: JSON.stringify(this.ctx.doc) });
4744
5272
  }
5273
+ /** Records the stored state of one cell, for undoing a single-cell edit. */
5274
+ recordCellSnapshot(rowId, colId) {
5275
+ if (this.isApplyingHistory || this.batching) return;
5276
+ this.pushUndo({ kind: "cells", sheetId: this.ctx.doc.id, data: JSON.stringify(this.cellHistory(this.ctx.doc, [[rowId, colId]])) });
5277
+ }
5278
+ cellHistory(doc, cells) {
5279
+ return {
5280
+ cells: cells.map(([rowId, colId]) => [rowId, colId, doc.cells[rowId]?.[colId] ?? null, doc.cells[rowId] !== void 0]),
5281
+ updatedAt: doc.updatedAt
5282
+ };
5283
+ }
4745
5284
  recordWorkbookSnapshot() {
4746
5285
  if (this.isApplyingHistory || this.batching) return;
4747
5286
  this.pushUndo({ kind: "workbook", data: JSON.stringify(this.workbookSnapshot()) });
@@ -4783,6 +5322,37 @@ var SpreadsheetController = class {
4783
5322
  if (!entry) return;
4784
5323
  this.isApplyingHistory = true;
4785
5324
  try {
5325
+ if (entry.kind === "cells") {
5326
+ const rt = this.findRuntimeById(entry.sheetId);
5327
+ if (!rt) return;
5328
+ const saved = JSON.parse(entry.data);
5329
+ const positions = saved.cells.map(([rowId, colId]) => [rowId, colId]);
5330
+ to.push({ kind: "cells", sheetId: rt.doc.id, data: JSON.stringify(this.cellHistory(rt.doc, positions)) });
5331
+ for (const [rowId, colId, cell, rowExisted] of saved.cells) {
5332
+ if (cell) {
5333
+ (rt.doc.cells[rowId] ??= {})[colId] = cell;
5334
+ } else if (rt.doc.cells[rowId]) {
5335
+ delete rt.doc.cells[rowId][colId];
5336
+ if (!rowExisted && Object.keys(rt.doc.cells[rowId]).length === 0) delete rt.doc.cells[rowId];
5337
+ }
5338
+ }
5339
+ if (saved.updatedAt !== void 0) rt.doc.updatedAt = saved.updatedAt;
5340
+ if (rt !== this.active) {
5341
+ this.ctx = this.active;
5342
+ this.activateRuntime(rt);
5343
+ }
5344
+ this.withSheet(rt, () => {
5345
+ for (const [rowId, colId] of positions) {
5346
+ const raw = rt.doc.cells[rowId]?.[colId]?.raw;
5347
+ const cellKey2 = this.getCellKey(rowId, colId);
5348
+ this.registerFormula(cellKey2, raw?.startsWith("=") ? raw : null);
5349
+ this.dropStaleSpillEdges(cellKey2, raw ?? "");
5350
+ }
5351
+ this.recalculateWrittenCells(positions);
5352
+ });
5353
+ this.notifyChange();
5354
+ return;
5355
+ }
4786
5356
  if (entry.kind === "sheet") {
4787
5357
  const rt = this.findRuntimeById(entry.sheetId);
4788
5358
  if (!rt) return;
@@ -4999,7 +5569,7 @@ var SpreadsheetController = class {
4999
5569
  const changed = [];
5000
5570
  for (const key of this.cfDirty) {
5001
5571
  const located = this.locateCellKey(key);
5002
- if (located) changed.push({ row: located.row.index, col: this.document.columns.indexOf(located.col) });
5572
+ if (located) changed.push({ row: located.row.index, col: positionOf(this.document.columns, located.col.id) });
5003
5573
  }
5004
5574
  this.cfEngine.setSheet(sheet);
5005
5575
  this.cfEngine.update(rules, changed);
@@ -5606,10 +6176,180 @@ function getNameManagerRows(controller) {
5606
6176
  }).sort((a, b) => a.name.localeCompare(b.name) || a.scopeLabel.localeCompare(b.scopeLabel));
5607
6177
  }
5608
6178
 
6179
+ // src/spreadsheet/viewport.ts
6180
+ var SPREADSHEET_COLUMN_HEADER_HEIGHT = 28;
6181
+ var SPREADSHEET_ROW_HEADER_WIDTH = 48;
6182
+ var SPREADSHEET_GROUP_ROW_HEIGHT = 28;
6183
+ var SPREADSHEET_VIRTUAL_ROW_THRESHOLD = 200;
6184
+ var SPREADSHEET_VIRTUAL_COLUMN_THRESHOLD = 60;
6185
+ var SPREADSHEET_DEFAULT_VIEWPORT = { width: 1200, height: 600 };
6186
+ function measureSpreadsheetView(view) {
6187
+ const rowTops = new Float64Array(view.rows.length + 1);
6188
+ const rowPosition = /* @__PURE__ */ new Map();
6189
+ let frozenRows = 0;
6190
+ let top = 0;
6191
+ view.rows.forEach((viewRow, i) => {
6192
+ rowTops[i] = top;
6193
+ if (viewRow.kind === "row") {
6194
+ rowPosition.set(viewRow.index, i);
6195
+ if (viewRow.frozen && frozenRows === i) frozenRows = i + 1;
6196
+ top += viewRow.row.height || DEFAULT_ROW_HEIGHT;
6197
+ } else {
6198
+ top += SPREADSHEET_GROUP_ROW_HEIGHT;
6199
+ }
6200
+ });
6201
+ rowTops[view.rows.length] = top;
6202
+ const columnLefts = new Float64Array(view.columns.length + 1);
6203
+ const columnPosition = /* @__PURE__ */ new Map();
6204
+ let frozenColumns = 0;
6205
+ let left = 0;
6206
+ view.columns.forEach((viewCol, i) => {
6207
+ columnLefts[i] = left;
6208
+ columnPosition.set(viewCol.index, i);
6209
+ if (viewCol.frozen && frozenColumns === i) frozenColumns = i + 1;
6210
+ left += viewCol.column.width || DEFAULT_COLUMN_WIDTH;
6211
+ });
6212
+ columnLefts[view.columns.length] = left;
6213
+ return { rowTops, columnLefts, frozenRows, frozenColumns, rowPosition, columnPosition };
6214
+ }
6215
+ function shouldVirtualizeSpreadsheet(view, mode = "auto") {
6216
+ if (mode !== "auto") return mode;
6217
+ return view.rows.length > SPREADSHEET_VIRTUAL_ROW_THRESHOLD || view.columns.length > SPREADSHEET_VIRTUAL_COLUMN_THRESHOLD;
6218
+ }
6219
+ function fullSpreadsheetViewWindow(metrics) {
6220
+ return {
6221
+ virtual: false,
6222
+ frozenRows: metrics.frozenRows,
6223
+ rowStart: metrics.frozenRows,
6224
+ rowEnd: metrics.rowTops.length - 1,
6225
+ topSpace: 0,
6226
+ bottomSpace: 0,
6227
+ frozenColumns: metrics.frozenColumns,
6228
+ colStart: metrics.frozenColumns,
6229
+ colEnd: metrics.columnLefts.length - 1,
6230
+ leftSpace: 0,
6231
+ rightSpace: 0
6232
+ };
6233
+ }
6234
+ function entryAt(edges, value, lo, hi) {
6235
+ while (lo < hi) {
6236
+ const mid = lo + hi >>> 1;
6237
+ if (edges[mid + 1] <= value) lo = mid + 1;
6238
+ else hi = mid;
6239
+ }
6240
+ return lo;
6241
+ }
6242
+ function visibleSlice(edges, frozen, from, to, overscan) {
6243
+ const count = edges.length - 1;
6244
+ if (count <= frozen) return [frozen, frozen];
6245
+ const first2 = entryAt(edges, from, frozen, count);
6246
+ const last = entryAt(edges, Math.max(from, to - 1), first2, count);
6247
+ return [Math.max(frozen, first2 - overscan), Math.min(count, last + 1 + overscan)];
6248
+ }
6249
+ function getSpreadsheetViewWindow(metrics, viewport, options = {}) {
6250
+ const overscanRows = options.overscanRows ?? 8;
6251
+ const overscanColumns = options.overscanColumns ?? 2;
6252
+ const height = viewport.height > 0 ? viewport.height : SPREADSHEET_DEFAULT_VIEWPORT.height;
6253
+ const width = viewport.width > 0 ? viewport.width : SPREADSHEET_DEFAULT_VIEWPORT.width;
6254
+ const { rowTops, columnLefts, frozenRows, frozenColumns } = metrics;
6255
+ const rowCount = rowTops.length - 1;
6256
+ const colCount = columnLefts.length - 1;
6257
+ const top = Math.max(0, viewport.scrollTop);
6258
+ let [rowStart, rowEnd] = visibleSlice(rowTops, frozenRows, top, top + Math.max(0, height - SPREADSHEET_COLUMN_HEADER_HEIGHT), overscanRows);
6259
+ const left = Math.max(0, viewport.scrollLeft);
6260
+ let [colStart, colEnd] = visibleSlice(columnLefts, frozenColumns, left, left + Math.max(0, width - SPREADSHEET_ROW_HEADER_WIDTH), overscanColumns);
6261
+ const merges = options.merges ?? [];
6262
+ for (let changed = merges.length > 0, guard = 0; changed && guard < 8; guard++) {
6263
+ changed = false;
6264
+ for (const raw of merges) {
6265
+ const merge = normalizeRange(raw);
6266
+ const rows = displayedSpan(metrics.rowPosition, merge.startRow, merge.endRow);
6267
+ const cols = displayedSpan(metrics.columnPosition, merge.startCol, merge.endCol);
6268
+ if (!rows || !cols) continue;
6269
+ const rowsMeet = rows[1] >= rowStart && rows[0] < rowEnd;
6270
+ const colsMeet = cols[1] >= colStart && cols[0] < colEnd;
6271
+ if (!rowsMeet && !(rows[1] < frozenRows)) continue;
6272
+ if (!colsMeet && !(cols[1] < frozenColumns)) continue;
6273
+ if (rowsMeet) {
6274
+ const s = Math.max(frozenRows, rows[0]);
6275
+ const e = rows[1] + 1;
6276
+ if (s < rowStart || e > rowEnd) {
6277
+ rowStart = Math.min(rowStart, s);
6278
+ rowEnd = Math.max(rowEnd, e);
6279
+ changed = true;
6280
+ }
6281
+ }
6282
+ if (colsMeet) {
6283
+ const s = Math.max(frozenColumns, cols[0]);
6284
+ const e = cols[1] + 1;
6285
+ if (s < colStart || e > colEnd) {
6286
+ colStart = Math.min(colStart, s);
6287
+ colEnd = Math.max(colEnd, e);
6288
+ changed = true;
6289
+ }
6290
+ }
6291
+ }
6292
+ }
6293
+ return {
6294
+ virtual: true,
6295
+ frozenRows,
6296
+ rowStart,
6297
+ rowEnd,
6298
+ topSpace: rowTops[rowStart] - rowTops[frozenRows],
6299
+ bottomSpace: rowTops[rowCount] - rowTops[rowEnd],
6300
+ frozenColumns,
6301
+ colStart,
6302
+ colEnd,
6303
+ leftSpace: columnLefts[colStart] - columnLefts[frozenColumns],
6304
+ rightSpace: columnLefts[colCount] - columnLefts[colEnd]
6305
+ };
6306
+ }
6307
+ function displayedSpan(positions, from, to) {
6308
+ let lo = Infinity;
6309
+ let hi = -Infinity;
6310
+ for (let i = from; i <= to; i++) {
6311
+ const pos = positions.get(i);
6312
+ if (pos === void 0) continue;
6313
+ if (pos < lo) lo = pos;
6314
+ if (pos > hi) hi = pos;
6315
+ }
6316
+ return lo === Infinity ? null : [lo, hi];
6317
+ }
6318
+ function getSpreadsheetScrollToCell(metrics, viewport, cell) {
6319
+ const height = viewport.height > 0 ? viewport.height : SPREADSHEET_DEFAULT_VIEWPORT.height;
6320
+ const width = viewport.width > 0 ? viewport.width : SPREADSHEET_DEFAULT_VIEWPORT.width;
6321
+ const result = {};
6322
+ const rowPos = metrics.rowPosition.get(cell.row);
6323
+ if (rowPos !== void 0 && rowPos >= metrics.frozenRows) {
6324
+ const frozenHeight = metrics.rowTops[metrics.frozenRows];
6325
+ const top = metrics.rowTops[rowPos];
6326
+ const bottom = metrics.rowTops[rowPos + 1];
6327
+ const visibleHeight = height - SPREADSHEET_COLUMN_HEADER_HEIGHT;
6328
+ if (top < viewport.scrollTop + frozenHeight) result.scrollTop = Math.max(0, top - frozenHeight);
6329
+ else if (bottom > viewport.scrollTop + visibleHeight) result.scrollTop = Math.max(0, bottom - visibleHeight);
6330
+ }
6331
+ const colPos = metrics.columnPosition.get(cell.col);
6332
+ if (colPos !== void 0 && colPos >= metrics.frozenColumns) {
6333
+ const frozenWidth = metrics.columnLefts[metrics.frozenColumns];
6334
+ const left = metrics.columnLefts[colPos];
6335
+ const right = metrics.columnLefts[colPos + 1];
6336
+ const visibleWidth = width - SPREADSHEET_ROW_HEADER_WIDTH;
6337
+ if (left < viewport.scrollLeft + frozenWidth) result.scrollLeft = Math.max(0, left - frozenWidth);
6338
+ else if (right > viewport.scrollLeft + visibleWidth) result.scrollLeft = Math.max(0, right - visibleWidth);
6339
+ }
6340
+ return result.scrollTop === void 0 && result.scrollLeft === void 0 ? null : result;
6341
+ }
6342
+ function sameSpreadsheetViewWindow(a, b) {
6343
+ return a.virtual === b.virtual && a.rowStart === b.rowStart && a.rowEnd === b.rowEnd && a.colStart === b.colStart && a.colEnd === b.colEnd && a.topSpace === b.topSpace && a.bottomSpace === b.bottomSpace && a.leftSpace === b.leftSpace && a.rightSpace === b.rightSpace;
6344
+ }
6345
+
5609
6346
  export {
5610
6347
  extractNameDependencies,
5611
6348
  columnDependencyKey,
5612
6349
  extractCellDependencies,
6350
+ extractReferenceDependencies,
6351
+ rangeDependencyKey,
6352
+ IntervalIndex,
5613
6353
  DependencyGraph,
5614
6354
  shiftFormulaReferences,
5615
6355
  mapFormulaReferences,
@@ -5721,6 +6461,18 @@ export {
5721
6461
  importFileIntoSpreadsheet,
5722
6462
  getSheetTabMenuItems,
5723
6463
  sheetDropIndex,
5724
- getNameManagerRows
6464
+ getNameManagerRows,
6465
+ SPREADSHEET_COLUMN_HEADER_HEIGHT,
6466
+ SPREADSHEET_ROW_HEADER_WIDTH,
6467
+ SPREADSHEET_GROUP_ROW_HEIGHT,
6468
+ SPREADSHEET_VIRTUAL_ROW_THRESHOLD,
6469
+ SPREADSHEET_VIRTUAL_COLUMN_THRESHOLD,
6470
+ SPREADSHEET_DEFAULT_VIEWPORT,
6471
+ measureSpreadsheetView,
6472
+ shouldVirtualizeSpreadsheet,
6473
+ fullSpreadsheetViewWindow,
6474
+ getSpreadsheetViewWindow,
6475
+ getSpreadsheetScrollToCell,
6476
+ sameSpreadsheetViewWindow
5725
6477
  };
5726
- //# sourceMappingURL=chunk-RR7F5GUM.js.map
6478
+ //# sourceMappingURL=chunk-X7HXL2SB.js.map