@bilig/headless 0.1.101 → 0.2.0

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.
@@ -1,4 +1,4 @@
1
- import { SpreadsheetEngine, makeCellKey } from '@bilig/core';
1
+ import { SpreadsheetEngine, attachRuntimeSnapshot, makeCellKey, readRuntimeSnapshot, } from '@bilig/core';
2
2
  import { ErrorCode, MAX_COLS, MAX_ROWS, ValueTag, } from '@bilig/protocol';
3
3
  import { excelSerialToDateParts, formatAddress, formatRangeAddress, installExternalFunctionAdapter, isArrayValue, isCellReferenceText, parseCellAddress, parseFormula, parseRangeAddress, serializeFormula, translateFormulaReferences, } from '@bilig/formula';
4
4
  import { loadInitialMixedSheet, tryLoadInitialLiteralSheet } from './initial-sheet-load.js';
@@ -6,6 +6,7 @@ import { orderWorkPaperCellChanges } from './change-order.js';
6
6
  import { WorkPaperConfigValueTooBigError, WorkPaperConfigValueTooSmallError, WorkPaperEvaluationSuspendedError, WorkPaperExpectedValueOfTypeError, WorkPaperOperationError, WorkPaperParseError, WorkPaperSheetError, WorkPaperExpectedOneOfValuesError, WorkPaperFunctionPluginValidationError, WorkPaperInvalidArgumentsError, WorkPaperLanguageAlreadyRegisteredError, WorkPaperLanguageNotRegisteredError, WorkPaperNamedExpressionDoesNotExistError, WorkPaperNamedExpressionNameIsAlreadyTakenError, WorkPaperNamedExpressionNameIsInvalidError, WorkPaperNoOperationToRedoError, WorkPaperNoOperationToUndoError, WorkPaperNoRelativeAddressesAllowedError, WorkPaperNoSheetWithIdError, WorkPaperNoSheetWithNameError, WorkPaperNotAFormulaError, WorkPaperNothingToPasteError, WorkPaperSheetNameAlreadyTakenError, WorkPaperSheetSizeLimitExceededError, WorkPaperUnableToParseError, } from './work-paper-errors.js';
7
7
  import { buildMatrixMutationPlan } from './matrix-mutation-plan.js';
8
8
  import { captureTrackedEngineEvent } from './tracked-engine-event-refs.js';
9
+ import { materializeTrackedIndexChangesWithMetadata } from './tracked-cell-index-changes.js';
9
10
  import { calculateWorkPaperFormulaInScratchWorkbook } from './work-paper-scratch-evaluator.js';
10
11
  import { replaceWorkPaperSheetContent } from './work-paper-sheet-replacement.js';
11
12
  const EMPTY_NAMED_EXPRESSION_VALUES = new Map();
@@ -810,10 +811,18 @@ export class WorkPaper {
810
811
  }
811
812
  static buildFromSheets(sheets, configInput = {}, namedExpressions = []) {
812
813
  const workbook = new WorkPaper(configInput);
814
+ const runtimeSnapshot = namedExpressions.length === 0 ? readRuntimeSnapshot(sheets) : undefined;
815
+ const runtimeSnapshotMatchesSheets = runtimeSnapshot !== undefined &&
816
+ runtimeSnapshot.sheets.length === Object.keys(sheets).length &&
817
+ runtimeSnapshot.sheets.every((sheet) => Object.prototype.hasOwnProperty.call(sheets, sheet.name));
813
818
  Object.entries(sheets).forEach(([sheetName, sheet]) => {
814
819
  validateSheetWithinLimits(sheetName, sheet, workbook.config);
815
820
  });
816
821
  workbook.withEngineEventCaptureDisabled(() => {
822
+ if (runtimeSnapshot && runtimeSnapshotMatchesSheets) {
823
+ workbook.engine.importSnapshot(runtimeSnapshot);
824
+ return;
825
+ }
817
826
  Object.keys(sheets).forEach((sheetName) => {
818
827
  workbook.engine.createSheet(sheetName);
819
828
  });
@@ -1023,6 +1032,16 @@ export class WorkPaper {
1023
1032
  lastMetrics: structuredClone(this.engine.getLastMetrics()),
1024
1033
  };
1025
1034
  }
1035
+ getPerformanceCounters() {
1036
+ this.assertNotDisposed();
1037
+ const counterAwareEngine = this.engine;
1038
+ return structuredClone(counterAwareEngine.getPerformanceCounters());
1039
+ }
1040
+ resetPerformanceCounters() {
1041
+ this.assertNotDisposed();
1042
+ const counterAwareEngine = this.engine;
1043
+ counterAwareEngine.resetPerformanceCounters();
1044
+ }
1026
1045
  rebuildAndRecalculate() {
1027
1046
  this.assertNotDisposed();
1028
1047
  if (this.shouldSuppressEvents()) {
@@ -1312,7 +1331,9 @@ export class WorkPaper {
1312
1331
  return Object.fromEntries(this.listSheetRecords().map((sheet) => [sheet.name, this.getSheetFormulas(sheet.id)]));
1313
1332
  }
1314
1333
  getAllSheetsSerialized() {
1315
- return Object.fromEntries(this.listSheetRecords().map((sheet) => [sheet.name, this.getSheetSerialized(sheet.id)]));
1334
+ const serialized = Object.fromEntries(this.listSheetRecords().map((sheet) => [sheet.name, this.getSheetSerialized(sheet.id)]));
1335
+ attachRuntimeSnapshot(serialized, this.engine.exportSnapshot());
1336
+ return serialized;
1316
1337
  }
1317
1338
  getAllSheetsDimensions() {
1318
1339
  return Object.fromEntries(this.listSheetRecords().map((sheet) => [sheet.name, this.getSheetDimensions(sheet.id)]));
@@ -1864,6 +1885,12 @@ export class WorkPaper {
1864
1885
  if (!this.isItPossibleToAddRows(sheetId, ...indexes)) {
1865
1886
  throw new WorkPaperOperationError('Rows cannot be added');
1866
1887
  }
1888
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
1889
+ const [start, amount] = indexes[0];
1890
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
1891
+ this.engine.insertRows(this.sheetName(sheetId), start, amount);
1892
+ });
1893
+ }
1867
1894
  return this.batchStructuralChanges(() => {
1868
1895
  indexes.forEach(([start, amount]) => {
1869
1896
  this.engine.insertRows(this.sheetName(sheetId), start, amount);
@@ -1875,6 +1902,12 @@ export class WorkPaper {
1875
1902
  if (!this.isItPossibleToRemoveRows(sheetId, ...indexes)) {
1876
1903
  throw new WorkPaperOperationError('Rows cannot be removed');
1877
1904
  }
1905
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
1906
+ const [start, amount] = indexes[0];
1907
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
1908
+ this.engine.deleteRows(this.sheetName(sheetId), start, amount);
1909
+ });
1910
+ }
1878
1911
  return this.batchStructuralChanges(() => {
1879
1912
  indexes
1880
1913
  .toSorted((left, right) => right[0] - left[0])
@@ -1888,6 +1921,12 @@ export class WorkPaper {
1888
1921
  if (!this.isItPossibleToAddColumns(sheetId, ...indexes)) {
1889
1922
  throw new WorkPaperOperationError('Columns cannot be added');
1890
1923
  }
1924
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
1925
+ const [start, amount] = indexes[0];
1926
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
1927
+ this.engine.insertColumns(this.sheetName(sheetId), start, amount);
1928
+ });
1929
+ }
1891
1930
  return this.batchStructuralChanges(() => {
1892
1931
  indexes.forEach(([start, amount]) => {
1893
1932
  this.engine.insertColumns(this.sheetName(sheetId), start, amount);
@@ -1899,6 +1938,12 @@ export class WorkPaper {
1899
1938
  if (!this.isItPossibleToRemoveColumns(sheetId, ...indexes)) {
1900
1939
  throw new WorkPaperOperationError('Columns cannot be removed');
1901
1940
  }
1941
+ if (indexes.length === 1 && this.canUseTrackedStructuralFastPath()) {
1942
+ const [start, amount] = indexes[0];
1943
+ return this.captureTrackedChangesWithoutVisibilityCache(() => {
1944
+ this.engine.deleteColumns(this.sheetName(sheetId), start, amount);
1945
+ });
1946
+ }
1902
1947
  return this.batchStructuralChanges(() => {
1903
1948
  indexes
1904
1949
  .toSorted((left, right) => right[0] - left[0])
@@ -1926,7 +1971,7 @@ export class WorkPaper {
1926
1971
  throw new WorkPaperOperationError('Rows cannot be moved');
1927
1972
  }
1928
1973
  return this.canUseTrackedStructuralFastPath()
1929
- ? this.batchStructuralChanges(() => {
1974
+ ? this.captureTrackedChangesWithoutVisibilityCache(() => {
1930
1975
  this.engine.moveRows(this.sheetName(sheetId), start, count, target);
1931
1976
  })
1932
1977
  : this.captureChanges(undefined, () => {
@@ -1938,7 +1983,7 @@ export class WorkPaper {
1938
1983
  throw new WorkPaperOperationError('Columns cannot be moved');
1939
1984
  }
1940
1985
  return this.canUseTrackedStructuralFastPath()
1941
- ? this.batchStructuralChanges(() => {
1986
+ ? this.captureTrackedChangesWithoutVisibilityCache(() => {
1942
1987
  this.engine.moveColumns(this.sheetName(sheetId), start, count, target);
1943
1988
  })
1944
1989
  : this.captureChanges(undefined, () => {
@@ -2480,30 +2525,85 @@ export class WorkPaper {
2480
2525
  });
2481
2526
  return orderWorkPaperCellChanges(cellChanges, this.listSheetRecords());
2482
2527
  }
2483
- readTrackedCellChange(cellIndex) {
2484
- const sheetId = this.engine.workbook.cellStore.sheetIds[cellIndex];
2485
- const row = this.engine.workbook.cellStore.rows[cellIndex];
2486
- const col = this.engine.workbook.cellStore.cols[cellIndex];
2487
- if (sheetId === undefined || row === undefined || col === undefined) {
2488
- return undefined;
2528
+ materializeTrackedEventChanges(event) {
2529
+ if (event.patches && event.patches.length > 0) {
2530
+ const cellPatches = event.patches.filter((patch) => patch.kind === 'cell');
2531
+ return { changes: cellPatches, canReusePublicChanges: false, ordered: false };
2489
2532
  }
2490
- const sheetName = this.engine.workbook.getSheetNameById(sheetId);
2491
- if (sheetName === undefined) {
2533
+ const materialized = materializeTrackedIndexChangesWithMetadata(this.engine, event.changedCellIndices, {
2534
+ explicitChangedCount: event.explicitChangedCount,
2535
+ });
2536
+ return {
2537
+ changes: materialized.changes,
2538
+ canReusePublicChanges: true,
2539
+ ordered: materialized.ordered,
2540
+ };
2541
+ }
2542
+ readSingleTrackedCellChange(cellIndex) {
2543
+ const cellStore = this.engine.workbook.cellStore;
2544
+ const sheetId = cellStore.sheetIds[cellIndex];
2545
+ if (sheetId === undefined) {
2492
2546
  return undefined;
2493
2547
  }
2548
+ const sheet = this.engine.workbook.getSheetById(sheetId);
2549
+ const sheetName = sheet?.name ?? this.engine.workbook.getSheetNameById(sheetId);
2550
+ let row;
2551
+ let col;
2552
+ if (!sheet || sheet.structureVersion === 1) {
2553
+ row = cellStore.rows[cellIndex];
2554
+ col = cellStore.cols[cellIndex];
2555
+ }
2556
+ else {
2557
+ const position = this.engine.workbook.getCellPosition(cellIndex);
2558
+ if (!position) {
2559
+ return undefined;
2560
+ }
2561
+ row = position.row;
2562
+ col = position.col;
2563
+ }
2564
+ const tag = cellStore.tags[cellIndex] ?? ValueTag.Empty;
2565
+ let newValue;
2566
+ switch (tag) {
2567
+ case ValueTag.Number:
2568
+ newValue = { tag: ValueTag.Number, value: cellStore.numbers[cellIndex] ?? 0 };
2569
+ break;
2570
+ case ValueTag.Boolean:
2571
+ newValue = { tag: ValueTag.Boolean, value: (cellStore.numbers[cellIndex] ?? 0) !== 0 };
2572
+ break;
2573
+ case ValueTag.String:
2574
+ newValue = cellStore.getValue(cellIndex, (stringId) => this.engine.strings.get(stringId));
2575
+ break;
2576
+ case ValueTag.Error:
2577
+ newValue = { tag: ValueTag.Error, code: cellStore.errors[cellIndex] };
2578
+ break;
2579
+ case ValueTag.Empty:
2580
+ default:
2581
+ newValue = { tag: ValueTag.Empty };
2582
+ break;
2583
+ }
2494
2584
  return {
2495
2585
  kind: 'cell',
2496
2586
  address: { sheet: sheetId, row, col },
2497
2587
  sheetName,
2498
2588
  a1: formatAddress(row, col),
2499
- newValue: this.engine.workbook.cellStore.getValue(cellIndex, (id) => this.engine.strings.get(id)),
2589
+ newValue,
2500
2590
  };
2501
2591
  }
2502
- computeCellChangesFromTrackedEvents(beforeVisibility, events) {
2592
+ computeCellChangesFromTrackedEvents(beforeVisibility, events, updateVisibility = true) {
2503
2593
  if (events.some((event) => event.invalidation === 'full')) {
2504
2594
  return null;
2505
2595
  }
2506
2596
  const nextVisibility = beforeVisibility;
2597
+ const sheetOrders = new Map();
2598
+ const sheetOrderFor = (sheetId) => {
2599
+ const existing = sheetOrders.get(sheetId);
2600
+ if (existing !== undefined) {
2601
+ return existing;
2602
+ }
2603
+ const order = this.sheetRecord(sheetId).order;
2604
+ sheetOrders.set(sheetId, order);
2605
+ return order;
2606
+ };
2507
2607
  const ensureMutableSheet = (sheetId, sheetName) => {
2508
2608
  const existing = nextVisibility.get(sheetId);
2509
2609
  if (existing) {
@@ -2520,79 +2620,96 @@ export class WorkPaper {
2520
2620
  };
2521
2621
  if (events.length === 1) {
2522
2622
  const event = events[0];
2523
- let hasDuplicateCellKey = false;
2524
- if (event.changedCellIndices.length <= 4) {
2525
- for (let index = 0; index < event.changedCellIndices.length; index += 1) {
2526
- const change = this.readTrackedCellChange(event.changedCellIndices[index]);
2527
- if (!change) {
2528
- continue;
2529
- }
2530
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2531
- for (let priorIndex = 0; priorIndex < index; priorIndex += 1) {
2532
- const prior = this.readTrackedCellChange(event.changedCellIndices[priorIndex]);
2533
- if (!prior) {
2534
- continue;
2623
+ if (event.invalidation !== 'full' &&
2624
+ event.patches === undefined &&
2625
+ event.changedCellIndices.length === 1 &&
2626
+ event.explicitChangedCount === 1 &&
2627
+ event.changedInputCount === 1 &&
2628
+ !event.hasInvalidatedRanges &&
2629
+ !event.hasInvalidatedRows &&
2630
+ !event.hasInvalidatedColumns) {
2631
+ const change = this.readSingleTrackedCellChange(event.changedCellIndices[0]);
2632
+ if (change) {
2633
+ if (updateVisibility) {
2634
+ const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
2635
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2636
+ if (change.newValue.tag === ValueTag.Empty) {
2637
+ sheet.cells.delete(cellKey);
2535
2638
  }
2536
- if (makeCellKey(prior.address.sheet, prior.address.row, prior.address.col) === cellKey) {
2537
- hasDuplicateCellKey = true;
2538
- break;
2639
+ else {
2640
+ sheet.cells.set(cellKey, change.newValue);
2539
2641
  }
2540
2642
  }
2541
- if (hasDuplicateCellKey) {
2542
- break;
2543
- }
2643
+ return { changes: [change], nextVisibility };
2544
2644
  }
2545
2645
  }
2546
- else {
2547
- const seenCellKeys = new Set();
2548
- for (let index = 0; index < event.changedCellIndices.length; index += 1) {
2549
- const change = this.readTrackedCellChange(event.changedCellIndices[index]);
2550
- if (!change) {
2551
- continue;
2552
- }
2553
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2646
+ const materializedEventChanges = this.materializeTrackedEventChanges(event);
2647
+ const eventChanges = materializedEventChanges.changes;
2648
+ if (!updateVisibility && materializedEventChanges.canReusePublicChanges && materializedEventChanges.ordered) {
2649
+ return {
2650
+ changes: [...eventChanges],
2651
+ nextVisibility,
2652
+ };
2653
+ }
2654
+ const directChanges = [];
2655
+ const seenCellKeys = eventChanges.length > 4 && eventChanges.length <= 64 ? new Set() : undefined;
2656
+ const smallCellKeys = eventChanges.length > 1 && eventChanges.length <= 4 ? [] : undefined;
2657
+ let hasDuplicateCellKey = false;
2658
+ let alreadySorted = true;
2659
+ let previousSheetOrder = -1;
2660
+ let previousRow = -1;
2661
+ let previousCol = -1;
2662
+ for (let index = 0; index < eventChanges.length; index += 1) {
2663
+ const change = eventChanges[index];
2664
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2665
+ if (seenCellKeys) {
2554
2666
  if (seenCellKeys.has(cellKey)) {
2555
2667
  hasDuplicateCellKey = true;
2556
2668
  break;
2557
2669
  }
2558
2670
  seenCellKeys.add(cellKey);
2559
2671
  }
2560
- }
2561
- if (!hasDuplicateCellKey) {
2562
- const directChanges = [];
2563
- let alreadySorted = true;
2564
- let previousSheetOrder = -1;
2565
- let previousRow = -1;
2566
- let previousCol = -1;
2567
- for (let index = 0; index < event.changedCellIndices.length; index += 1) {
2568
- const change = this.readTrackedCellChange(event.changedCellIndices[index]);
2569
- if (!change) {
2570
- continue;
2672
+ else if (smallCellKeys) {
2673
+ for (let priorIndex = 0; priorIndex < index; priorIndex += 1) {
2674
+ if (smallCellKeys[priorIndex] === cellKey) {
2675
+ hasDuplicateCellKey = true;
2676
+ break;
2677
+ }
2571
2678
  }
2572
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2573
- const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
2574
- if (sheet.order < previousSheetOrder ||
2575
- (sheet.order === previousSheetOrder &&
2576
- (change.address.row < previousRow || (change.address.row === previousRow && change.address.col < previousCol)))) {
2577
- alreadySorted = false;
2679
+ if (hasDuplicateCellKey) {
2680
+ break;
2578
2681
  }
2682
+ smallCellKeys[index] = cellKey;
2683
+ }
2684
+ const sheet = updateVisibility ? ensureMutableSheet(change.address.sheet, change.sheetName) : undefined;
2685
+ const sheetOrder = sheet?.order ?? sheetOrderFor(change.address.sheet);
2686
+ if (sheetOrder < previousSheetOrder ||
2687
+ (sheetOrder === previousSheetOrder &&
2688
+ (change.address.row < previousRow || (change.address.row === previousRow && change.address.col < previousCol)))) {
2689
+ alreadySorted = false;
2690
+ }
2691
+ if (sheet) {
2579
2692
  if (change.newValue.tag === ValueTag.Empty) {
2580
2693
  sheet.cells.delete(cellKey);
2581
2694
  }
2582
2695
  else {
2583
2696
  sheet.cells.set(cellKey, change.newValue);
2584
2697
  }
2585
- directChanges[index] = {
2698
+ }
2699
+ directChanges[index] = materializedEventChanges.canReusePublicChanges
2700
+ ? change
2701
+ : {
2586
2702
  kind: 'cell',
2587
2703
  address: change.address,
2588
2704
  sheetName: change.sheetName,
2589
2705
  a1: change.a1,
2590
2706
  newValue: change.newValue,
2591
2707
  };
2592
- previousSheetOrder = sheet.order;
2593
- previousRow = change.address.row;
2594
- previousCol = change.address.col;
2595
- }
2708
+ previousSheetOrder = sheetOrder;
2709
+ previousRow = change.address.row;
2710
+ previousCol = change.address.col;
2711
+ }
2712
+ if (!hasDuplicateCellKey) {
2596
2713
  return {
2597
2714
  changes: alreadySorted
2598
2715
  ? directChanges
@@ -2603,11 +2720,9 @@ export class WorkPaper {
2603
2720
  }
2604
2721
  const latestChangesByKey = new Map();
2605
2722
  for (const event of events) {
2606
- for (let index = 0; index < event.changedCellIndices.length; index += 1) {
2607
- const change = this.readTrackedCellChange(event.changedCellIndices[index]);
2608
- if (!change) {
2609
- continue;
2610
- }
2723
+ const eventChanges = this.materializeTrackedEventChanges(event).changes;
2724
+ for (let index = 0; index < eventChanges.length; index += 1) {
2725
+ const change = eventChanges[index];
2611
2726
  const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2612
2727
  latestChangesByKey.delete(cellKey);
2613
2728
  latestChangesByKey.set(cellKey, {
@@ -2619,14 +2734,16 @@ export class WorkPaper {
2619
2734
  });
2620
2735
  }
2621
2736
  }
2622
- for (const change of latestChangesByKey.values()) {
2623
- const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2624
- const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
2625
- if (change.newValue.tag === ValueTag.Empty) {
2626
- sheet.cells.delete(cellKey);
2627
- }
2628
- else {
2629
- sheet.cells.set(cellKey, change.newValue);
2737
+ if (updateVisibility) {
2738
+ for (const change of latestChangesByKey.values()) {
2739
+ const cellKey = makeCellKey(change.address.sheet, change.address.row, change.address.col);
2740
+ const sheet = ensureMutableSheet(change.address.sheet, change.sheetName);
2741
+ if (change.newValue.tag === ValueTag.Empty) {
2742
+ sheet.cells.delete(cellKey);
2743
+ }
2744
+ else {
2745
+ sheet.cells.set(cellKey, change.newValue);
2746
+ }
2630
2747
  }
2631
2748
  }
2632
2749
  const directChanges = [...latestChangesByKey.values()];
@@ -2670,7 +2787,7 @@ export class WorkPaper {
2670
2787
  this.batchUsesTrackedFastPath = false;
2671
2788
  }
2672
2789
  computeTrackedChangesWithoutVisibilityCache(events) {
2673
- const fastPath = this.computeCellChangesFromTrackedEvents(new Map(), events);
2790
+ const fastPath = this.computeCellChangesFromTrackedEvents(new Map(), events, false);
2674
2791
  if (!fastPath) {
2675
2792
  throw new WorkPaperOperationError('Mutation emitted an unsupported invalidation pattern for tracked changes');
2676
2793
  }
@@ -2828,15 +2945,69 @@ export class WorkPaper {
2828
2945
  return record.ops;
2829
2946
  case 'single-op':
2830
2947
  return [record.op];
2948
+ case 'cell-mutations':
2949
+ return record.refs.flatMap((ref) => {
2950
+ const sheetName = this.getSheetName(ref.sheetId);
2951
+ if (!sheetName) {
2952
+ return [];
2953
+ }
2954
+ const address = formatAddress(ref.mutation.row, ref.mutation.col);
2955
+ switch (ref.mutation.kind) {
2956
+ case 'setCellValue':
2957
+ return [
2958
+ {
2959
+ kind: 'setCellValue',
2960
+ sheetName,
2961
+ address,
2962
+ value: ref.mutation.value,
2963
+ },
2964
+ ];
2965
+ case 'setCellFormula':
2966
+ return [
2967
+ {
2968
+ kind: 'setCellFormula',
2969
+ sheetName,
2970
+ address,
2971
+ formula: ref.mutation.formula,
2972
+ },
2973
+ ];
2974
+ case 'clearCell':
2975
+ return [
2976
+ {
2977
+ kind: 'clearCell',
2978
+ sheetName,
2979
+ address,
2980
+ },
2981
+ ];
2982
+ }
2983
+ });
2831
2984
  }
2832
2985
  }
2986
+ tryMergeTypedCellMutationHistory(entries) {
2987
+ if (entries.length === 0 ||
2988
+ entries.some((entry) => entry.forward.kind !== 'cell-mutations' || entry.inverse.kind !== 'cell-mutations')) {
2989
+ return null;
2990
+ }
2991
+ return {
2992
+ forward: {
2993
+ kind: 'cell-mutations',
2994
+ refs: entries.flatMap((entry) => (entry.forward.kind === 'cell-mutations' ? entry.forward.refs : [])),
2995
+ potentialNewCells: sumNumbers(entries.map((entry) => entry.forward.potentialNewCells)),
2996
+ },
2997
+ inverse: {
2998
+ kind: 'cell-mutations',
2999
+ refs: entries.toReversed().flatMap((entry) => (entry.inverse.kind === 'cell-mutations' ? entry.inverse.refs : [])),
3000
+ potentialNewCells: sumNumbers(entries.map((entry) => entry.inverse.potentialNewCells)),
3001
+ },
3002
+ };
3003
+ }
2833
3004
  mergeUndoHistory(startIndex) {
2834
3005
  const undoStack = this.getUndoStack();
2835
3006
  if (undoStack.length - startIndex <= 1) {
2836
3007
  return;
2837
3008
  }
2838
3009
  const entries = undoStack.splice(startIndex);
2839
- const merged = {
3010
+ const merged = this.tryMergeTypedCellMutationHistory(entries) ?? {
2840
3011
  forward: {
2841
3012
  kind: 'ops',
2842
3013
  ops: entries.flatMap((entry) => this.historyTransactionOps(entry.forward)),