@ricsam/formula-engine 0.2.13 → 0.2.15

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.
@@ -17,6 +17,7 @@ import { StyleManager } from "./managers/style-manager.mjs";
17
17
  import { CopyManager } from "./managers/copy-manager.mjs";
18
18
  import { ReferenceManager } from "./managers/reference-manager.mjs";
19
19
  import { RangeMetadataManager } from "./managers/range-metadata-manager.mjs";
20
+ import { UndoRedoManager } from "./managers/undo-redo-manager.mjs";
20
21
  import {
21
22
  ENGINE_SNAPSHOT_VERSION
22
23
  } from "./engine-snapshot.mjs";
@@ -51,6 +52,8 @@ class FormulaEngine {
51
52
  rangeMetadataManager;
52
53
  copyManager;
53
54
  referenceManager;
55
+ undoRedoManager;
56
+ historyCheckpointDepth = 0;
54
57
  _workbookManager;
55
58
  _namedExpressionManager;
56
59
  _tableManager;
@@ -60,7 +63,8 @@ class FormulaEngine {
60
63
  _dependencyManager;
61
64
  _styleManager;
62
65
  _rangeMetadataManager;
63
- constructor() {
66
+ constructor(options = {}) {
67
+ this.undoRedoManager = new UndoRedoManager(options.undoRedo);
64
68
  this.eventManager = new EventManager;
65
69
  this.workbookManager = new WorkbookManager;
66
70
  this.namedExpressionManager = new NamedExpressionManager;
@@ -84,8 +88,51 @@ class FormulaEngine {
84
88
  this._styleManager = this.styleManager;
85
89
  this._rangeMetadataManager = this.rangeMetadataManager;
86
90
  }
87
- static buildEmpty() {
88
- return new FormulaEngine;
91
+ static buildEmpty(options) {
92
+ return new FormulaEngine(options);
93
+ }
94
+ undo() {
95
+ if (!this.undoRedoManager.canUndo()) {
96
+ return false;
97
+ }
98
+ const target = this.undoRedoManager.popUndo();
99
+ if (!target) {
100
+ return false;
101
+ }
102
+ const current = this.serializeHistorySnapshot();
103
+ this.restoreHistorySnapshot(target);
104
+ this.undoRedoManager.pushRedo(current);
105
+ this.emitUpdate();
106
+ return true;
107
+ }
108
+ redo() {
109
+ if (!this.undoRedoManager.canRedo()) {
110
+ return false;
111
+ }
112
+ const target = this.undoRedoManager.popRedo();
113
+ if (!target) {
114
+ return false;
115
+ }
116
+ const current = this.serializeHistorySnapshot();
117
+ this.restoreHistorySnapshot(target);
118
+ this.undoRedoManager.pushUndo(current);
119
+ this.emitUpdate();
120
+ return true;
121
+ }
122
+ canUndo() {
123
+ return this.undoRedoManager.canUndo();
124
+ }
125
+ canRedo() {
126
+ return this.undoRedoManager.canRedo();
127
+ }
128
+ getUndoRedoState() {
129
+ return this.undoRedoManager.getState();
130
+ }
131
+ clearUndoRedoHistory() {
132
+ this.undoRedoManager.clear();
133
+ }
134
+ transact(callback) {
135
+ return this.withUndoRedoCheckpoint(callback);
89
136
  }
90
137
  emitMutation(footprint) {
91
138
  this.evaluationManager.invalidateFromMutation(footprint);
@@ -94,6 +141,21 @@ class FormulaEngine {
94
141
  emitUpdate() {
95
142
  this.eventManager.emitUpdate();
96
143
  }
144
+ withUndoRedoCheckpoint(callback) {
145
+ if (this.historyCheckpointDepth > 0) {
146
+ return callback();
147
+ }
148
+ const before = this.serializeHistorySnapshot();
149
+ this.historyCheckpointDepth++;
150
+ try {
151
+ const result = callback();
152
+ const after = this.serializeHistorySnapshot();
153
+ this.undoRedoManager.recordMutation(before, after);
154
+ return result;
155
+ } finally {
156
+ this.historyCheckpointDepth--;
157
+ }
158
+ }
97
159
  getExistingSheetContent(opts) {
98
160
  const sheet = this.workbookManager.getSheet(opts);
99
161
  return sheet ? new Map(sheet.content) : undefined;
@@ -170,8 +232,10 @@ class FormulaEngine {
170
232
  return this.evaluationManager.evaluationResultToSerializedValue(result, cellAddress, debug);
171
233
  }
172
234
  setCellMetadata(address, metadata) {
173
- this.workbookManager.setCellMetadata(address, metadata);
174
- this.emitUpdate();
235
+ return this.withUndoRedoCheckpoint(() => {
236
+ this.workbookManager.setCellMetadata(address, metadata);
237
+ this.emitUpdate();
238
+ });
175
239
  }
176
240
  getCellMetadata(address) {
177
241
  const metadata = this.workbookManager.getCellMetadata(address);
@@ -181,29 +245,37 @@ class FormulaEngine {
181
245
  return this.workbookManager.getSheetMetadataSerialized(opts);
182
246
  }
183
247
  setSheetMetadata(opts, metadata) {
184
- this.workbookManager.setSheetMetadata(opts, metadata);
185
- this.emitUpdate();
248
+ return this.withUndoRedoCheckpoint(() => {
249
+ this.workbookManager.setSheetMetadata(opts, metadata);
250
+ this.emitUpdate();
251
+ });
186
252
  }
187
253
  getSheetMetadata(opts) {
188
254
  return this.workbookManager.getSheetMetadata(opts);
189
255
  }
190
256
  setWorkbookMetadata(workbookName, metadata) {
191
- this.workbookManager.setWorkbookMetadata(workbookName, metadata);
192
- this.emitUpdate();
257
+ return this.withUndoRedoCheckpoint(() => {
258
+ this.workbookManager.setWorkbookMetadata(workbookName, metadata);
259
+ this.emitUpdate();
260
+ });
193
261
  }
194
262
  getWorkbookMetadata(workbookName) {
195
263
  return this.workbookManager.getWorkbookMetadata(workbookName);
196
264
  }
197
265
  addRangeMetadata(metadata) {
198
- const id = this.rangeMetadataManager.addRangeMetadata(metadata);
199
- this.emitUpdate();
200
- return id;
266
+ return this.withUndoRedoCheckpoint(() => {
267
+ const id = this.rangeMetadataManager.addRangeMetadata(metadata);
268
+ this.emitUpdate();
269
+ return id;
270
+ });
201
271
  }
202
272
  removeRangeMetadata(id) {
203
- const removed = this.rangeMetadataManager.removeRangeMetadata(id);
204
- if (removed) {
205
- this.emitUpdate();
206
- }
273
+ return this.withUndoRedoCheckpoint(() => {
274
+ const removed = this.rangeMetadataManager.removeRangeMetadata(id);
275
+ if (removed) {
276
+ this.emitUpdate();
277
+ }
278
+ });
207
279
  }
208
280
  getAllRangeMetadata() {
209
281
  return this.rangeMetadataManager.getAllRangeMetadata();
@@ -215,17 +287,29 @@ class FormulaEngine {
215
287
  return this.rangeMetadataManager.getRangeMetadataIntersectingWithRange(range);
216
288
  }
217
289
  clearRangeMetadata(range) {
218
- this.rangeMetadataManager.clearRangeMetadataInRange(range);
219
- this.emitUpdate();
290
+ return this.withUndoRedoCheckpoint(() => {
291
+ this.rangeMetadataManager.clearRangeMetadataInRange(range);
292
+ this.emitUpdate();
293
+ });
220
294
  }
221
295
  createRef(address) {
222
- return this.referenceManager.createRef(address);
296
+ return this.withUndoRedoCheckpoint(() => {
297
+ const id = this.referenceManager.createRef(address);
298
+ this.emitUpdate();
299
+ return id;
300
+ });
223
301
  }
224
302
  getRefAddress(refId) {
225
303
  return this.referenceManager.getRefAddress(refId);
226
304
  }
227
305
  deleteRef(refId) {
228
- return this.referenceManager.deleteRef(refId);
306
+ return this.withUndoRedoCheckpoint(() => {
307
+ const deleted = this.referenceManager.deleteRef(refId);
308
+ if (deleted) {
309
+ this.emitUpdate();
310
+ }
311
+ return deleted;
312
+ });
229
313
  }
230
314
  getInvalidRefs() {
231
315
  return this.referenceManager.getInvalidRefs();
@@ -240,21 +324,25 @@ class FormulaEngine {
240
324
  throw new Error("Not implemented");
241
325
  }
242
326
  addNamedExpression(opts) {
243
- this.assertNamedExpressionScopeExists(opts);
244
- this.namedExpressionManager.addNamedExpression(opts);
245
- this.emitMutation({
246
- touchedCells: [],
247
- resourceKeys: [getNamedExpressionResourceKey(opts)]
248
- });
249
- }
250
- removeNamedExpression(opts) {
251
- const removed = this.namedExpressionManager.removeNamedExpression(opts);
252
- if (removed) {
327
+ return this.withUndoRedoCheckpoint(() => {
328
+ this.assertNamedExpressionScopeExists(opts);
329
+ this.namedExpressionManager.addNamedExpression(opts);
253
330
  this.emitMutation({
254
331
  touchedCells: [],
255
332
  resourceKeys: [getNamedExpressionResourceKey(opts)]
256
333
  });
257
- }
334
+ });
335
+ }
336
+ removeNamedExpression(opts) {
337
+ return this.withUndoRedoCheckpoint(() => {
338
+ const removed = this.namedExpressionManager.removeNamedExpression(opts);
339
+ if (removed) {
340
+ this.emitMutation({
341
+ touchedCells: [],
342
+ resourceKeys: [getNamedExpressionResourceKey(opts)]
343
+ });
344
+ }
345
+ });
258
346
  }
259
347
  hasNamedExpression(opts) {
260
348
  const scope = opts.sheetName && opts.workbookName ? {
@@ -268,140 +356,130 @@ class FormulaEngine {
268
356
  });
269
357
  }
270
358
  updateNamedExpression(opts) {
271
- this.namedExpressionManager.updateNamedExpression(opts);
272
- this.emitMutation({
273
- touchedCells: [],
274
- resourceKeys: [getNamedExpressionResourceKey(opts)]
359
+ return this.withUndoRedoCheckpoint(() => {
360
+ this.namedExpressionManager.updateNamedExpression(opts);
361
+ this.emitMutation({
362
+ touchedCells: [],
363
+ resourceKeys: [getNamedExpressionResourceKey(opts)]
364
+ });
275
365
  });
276
366
  }
277
367
  renameNamedExpression(opts) {
278
- this.namedExpressionManager.renameNamedExpression(opts);
279
- const changedCells = this.workbookManager.updateAllFormulas((formula) => renameNamedExpressionInFormula(formula, opts.expressionName, opts.newName));
280
- const changedNamedExpressions = this.namedExpressionManager.updateAllNamedExpressions((formula) => renameNamedExpressionInFormula(formula, opts.expressionName, opts.newName));
281
- this.emitMutation({
282
- touchedCells: buildFormulaTouchedCells(changedCells),
283
- resourceKeys: [
284
- getNamedExpressionResourceKey({
285
- expressionName: opts.expressionName,
286
- workbookName: opts.workbookName,
287
- sheetName: opts.sheetName
288
- }),
289
- getNamedExpressionResourceKey({
290
- expressionName: opts.newName,
291
- workbookName: opts.workbookName,
292
- sheetName: opts.sheetName
293
- }),
294
- ...changedNamedExpressions
295
- ]
368
+ return this.withUndoRedoCheckpoint(() => {
369
+ this.namedExpressionManager.renameNamedExpression(opts);
370
+ const changedCells = this.workbookManager.updateAllFormulas((formula) => renameNamedExpressionInFormula(formula, opts.expressionName, opts.newName));
371
+ const changedNamedExpressions = this.namedExpressionManager.updateAllNamedExpressions((formula) => renameNamedExpressionInFormula(formula, opts.expressionName, opts.newName));
372
+ this.emitMutation({
373
+ touchedCells: buildFormulaTouchedCells(changedCells),
374
+ resourceKeys: [
375
+ getNamedExpressionResourceKey({
376
+ expressionName: opts.expressionName,
377
+ workbookName: opts.workbookName,
378
+ sheetName: opts.sheetName
379
+ }),
380
+ getNamedExpressionResourceKey({
381
+ expressionName: opts.newName,
382
+ workbookName: opts.workbookName,
383
+ sheetName: opts.sheetName
384
+ }),
385
+ ...changedNamedExpressions
386
+ ]
387
+ });
296
388
  });
297
389
  }
298
390
  setNamedExpressions(opts) {
299
- const allExpressions = this.namedExpressionManager.getNamedExpressions();
300
- let previousExpressions;
301
- if (opts.type === "global") {
302
- previousExpressions = new Map(allExpressions.globalExpressions);
303
- } else if (opts.type === "workbook") {
304
- previousExpressions = new Map(allExpressions.workbookExpressions.get(opts.workbookName) || []);
305
- } else {
306
- const sheetExpressions = allExpressions.sheetExpressions.get(opts.workbookName)?.get(opts.sheetName);
307
- previousExpressions = new Map(sheetExpressions || []);
308
- }
309
- this.namedExpressionManager.setNamedExpressions(opts);
310
- const scope = opts.type === "global" ? {} : opts.type === "workbook" ? { workbookName: opts.workbookName } : {
311
- workbookName: opts.workbookName,
312
- sheetName: opts.sheetName
313
- };
314
- this.emitMutation({
315
- touchedCells: [],
316
- resourceKeys: [
317
- ...getNamedExpressionScopeResourceKeys(previousExpressions.keys(), scope),
318
- ...getNamedExpressionScopeResourceKeys(opts.expressions.keys(), scope)
319
- ]
391
+ return this.withUndoRedoCheckpoint(() => {
392
+ const allExpressions = this.namedExpressionManager.getNamedExpressions();
393
+ let previousExpressions;
394
+ if (opts.type === "global") {
395
+ previousExpressions = new Map(allExpressions.globalExpressions);
396
+ } else if (opts.type === "workbook") {
397
+ previousExpressions = new Map(allExpressions.workbookExpressions.get(opts.workbookName) || []);
398
+ } else {
399
+ const sheetExpressions = allExpressions.sheetExpressions.get(opts.workbookName)?.get(opts.sheetName);
400
+ previousExpressions = new Map(sheetExpressions || []);
401
+ }
402
+ this.namedExpressionManager.setNamedExpressions(opts);
403
+ const scope = opts.type === "global" ? {} : opts.type === "workbook" ? { workbookName: opts.workbookName } : {
404
+ workbookName: opts.workbookName,
405
+ sheetName: opts.sheetName
406
+ };
407
+ this.emitMutation({
408
+ touchedCells: [],
409
+ resourceKeys: [
410
+ ...getNamedExpressionScopeResourceKeys(previousExpressions.keys(), scope),
411
+ ...getNamedExpressionScopeResourceKeys(opts.expressions.keys(), scope)
412
+ ]
413
+ });
320
414
  });
321
415
  }
322
416
  addTable(props) {
323
- const table = this.tableManager.addTable({
324
- ...props,
325
- getCellValue: (cellAddress) => this.getCellValue(cellAddress)
326
- });
327
- this.emitMutation({
328
- touchedCells: buildTableTouchedCells(this.workbookManager, [table]),
329
- tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [table]),
330
- resourceKeys: [
331
- getTableResourceKey({
332
- workbookName: props.workbookName,
333
- tableName: props.tableName
334
- })
335
- ]
417
+ return this.withUndoRedoCheckpoint(() => {
418
+ const table = this.tableManager.addTable({
419
+ ...props,
420
+ getCellValue: (cellAddress) => this.getCellValue(cellAddress)
421
+ });
422
+ this.emitMutation({
423
+ touchedCells: buildTableTouchedCells(this.workbookManager, [table]),
424
+ tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [table]),
425
+ resourceKeys: [
426
+ getTableResourceKey({
427
+ workbookName: props.workbookName,
428
+ tableName: props.tableName
429
+ })
430
+ ]
431
+ });
336
432
  });
337
433
  }
338
434
  renameTable(workbookName, names) {
339
- const oldTable = this.tableManager.getTable({
340
- workbookName,
341
- name: names.oldName
342
- });
343
- const oldTableSnapshot = oldTable ? { ...oldTable, headers: new Map(oldTable.headers) } : undefined;
344
- this.tableManager.renameTable(workbookName, names);
345
- const changedCells = this.workbookManager.updateAllFormulas((formula) => renameTableInFormula(formula, names.oldName, names.newName));
346
- const changedNamedExpressions = this.namedExpressionManager.updateAllNamedExpressions((formula) => renameTableInFormula(formula, names.oldName, names.newName));
347
- const newTable = this.tableManager.getTable({
348
- workbookName,
349
- name: names.newName
350
- });
351
- this.emitMutation({
352
- touchedCells: mergeTouchedCells(buildTableTouchedCells(this.workbookManager, [oldTableSnapshot]), buildTableTouchedCells(this.workbookManager, [newTable]), buildFormulaTouchedCells(changedCells)),
353
- tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [oldTableSnapshot, newTable]),
354
- resourceKeys: [
355
- getTableResourceKey({
356
- workbookName,
357
- tableName: names.oldName
358
- }),
359
- getTableResourceKey({
360
- workbookName,
361
- tableName: names.newName
362
- }),
363
- ...changedNamedExpressions
364
- ]
435
+ return this.withUndoRedoCheckpoint(() => {
436
+ const oldTable = this.tableManager.getTable({
437
+ workbookName,
438
+ name: names.oldName
439
+ });
440
+ const oldTableSnapshot = oldTable ? { ...oldTable, headers: new Map(oldTable.headers) } : undefined;
441
+ this.tableManager.renameTable(workbookName, names);
442
+ const changedCells = this.workbookManager.updateAllFormulas((formula) => renameTableInFormula(formula, names.oldName, names.newName));
443
+ const changedNamedExpressions = this.namedExpressionManager.updateAllNamedExpressions((formula) => renameTableInFormula(formula, names.oldName, names.newName));
444
+ const newTable = this.tableManager.getTable({
445
+ workbookName,
446
+ name: names.newName
447
+ });
448
+ this.emitMutation({
449
+ touchedCells: mergeTouchedCells(buildTableTouchedCells(this.workbookManager, [oldTableSnapshot]), buildTableTouchedCells(this.workbookManager, [newTable]), buildFormulaTouchedCells(changedCells)),
450
+ tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [oldTableSnapshot, newTable]),
451
+ resourceKeys: [
452
+ getTableResourceKey({
453
+ workbookName,
454
+ tableName: names.oldName
455
+ }),
456
+ getTableResourceKey({
457
+ workbookName,
458
+ tableName: names.newName
459
+ }),
460
+ ...changedNamedExpressions
461
+ ]
462
+ });
365
463
  });
366
464
  }
367
465
  updateTable(opts) {
368
- const oldTable = this.tableManager.getTable({
369
- workbookName: opts.workbookName,
370
- name: opts.tableName
371
- });
372
- const oldTableSnapshot = oldTable ? { ...oldTable, headers: new Map(oldTable.headers) } : undefined;
373
- this.tableManager.updateTable({
374
- ...opts,
375
- getCellValue: (cellAddress) => this.getCellValue(cellAddress)
376
- });
377
- const newTable = this.tableManager.getTable({
378
- workbookName: opts.workbookName,
379
- name: opts.tableName
380
- });
381
- this.emitMutation({
382
- touchedCells: mergeTouchedCells(buildTableTouchedCells(this.workbookManager, [oldTableSnapshot]), buildTableTouchedCells(this.workbookManager, [newTable])),
383
- tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [oldTableSnapshot, newTable]),
384
- resourceKeys: [
385
- getTableResourceKey({
386
- workbookName: opts.workbookName,
387
- tableName: opts.tableName
388
- })
389
- ]
390
- });
391
- }
392
- removeTable(opts) {
393
- const oldTable = this.tableManager.getTable({
394
- workbookName: opts.workbookName,
395
- name: opts.tableName
396
- });
397
- const oldTableSnapshot = oldTable ? { ...oldTable, headers: new Map(oldTable.headers) } : undefined;
398
- const found = this.tableManager.removeTable(opts);
399
- if (found) {
466
+ return this.withUndoRedoCheckpoint(() => {
467
+ const oldTable = this.tableManager.getTable({
468
+ workbookName: opts.workbookName,
469
+ name: opts.tableName
470
+ });
471
+ const oldTableSnapshot = oldTable ? { ...oldTable, headers: new Map(oldTable.headers) } : undefined;
472
+ this.tableManager.updateTable({
473
+ ...opts,
474
+ getCellValue: (cellAddress) => this.getCellValue(cellAddress)
475
+ });
476
+ const newTable = this.tableManager.getTable({
477
+ workbookName: opts.workbookName,
478
+ name: opts.tableName
479
+ });
400
480
  this.emitMutation({
401
- touchedCells: buildTableTouchedCells(this.workbookManager, [
402
- oldTableSnapshot
403
- ]),
404
- tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [oldTableSnapshot]),
481
+ touchedCells: mergeTouchedCells(buildTableTouchedCells(this.workbookManager, [oldTableSnapshot]), buildTableTouchedCells(this.workbookManager, [newTable])),
482
+ tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [oldTableSnapshot, newTable]),
405
483
  resourceKeys: [
406
484
  getTableResourceKey({
407
485
  workbookName: opts.workbookName,
@@ -409,7 +487,31 @@ class FormulaEngine {
409
487
  })
410
488
  ]
411
489
  });
412
- }
490
+ });
491
+ }
492
+ removeTable(opts) {
493
+ return this.withUndoRedoCheckpoint(() => {
494
+ const oldTable = this.tableManager.getTable({
495
+ workbookName: opts.workbookName,
496
+ name: opts.tableName
497
+ });
498
+ const oldTableSnapshot = oldTable ? { ...oldTable, headers: new Map(oldTable.headers) } : undefined;
499
+ const found = this.tableManager.removeTable(opts);
500
+ if (found) {
501
+ this.emitMutation({
502
+ touchedCells: buildTableTouchedCells(this.workbookManager, [
503
+ oldTableSnapshot
504
+ ]),
505
+ tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [oldTableSnapshot]),
506
+ resourceKeys: [
507
+ getTableResourceKey({
508
+ workbookName: opts.workbookName,
509
+ tableName: opts.tableName
510
+ })
511
+ ]
512
+ });
513
+ }
514
+ });
413
515
  }
414
516
  getAllTables() {
415
517
  return Array.from(this.tableManager.tables.values()).flatMap((tables) => Array.from(tables.values()).map((table) => ({
@@ -430,20 +532,22 @@ class FormulaEngine {
430
532
  });
431
533
  }
432
534
  resetTables(tables) {
433
- const oldTables = this.getAllTables();
434
- const newTables = Array.from(tables.values()).flatMap((workbookTables) => Array.from(workbookTables.values()));
435
- const resourceKeys = new Set;
436
- for (const table of [...oldTables, ...newTables]) {
437
- resourceKeys.add(getTableResourceKey({
438
- workbookName: table.workbookName,
439
- tableName: table.name
440
- }));
441
- }
442
- this.tableManager.resetTables(tables);
443
- this.emitMutation({
444
- touchedCells: mergeTouchedCells(buildTableTouchedCells(this.workbookManager, oldTables), buildTableTouchedCells(this.workbookManager, newTables)),
445
- tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [...oldTables, ...newTables]),
446
- resourceKeys: Array.from(resourceKeys)
535
+ return this.withUndoRedoCheckpoint(() => {
536
+ const oldTables = this.getAllTables();
537
+ const newTables = Array.from(tables.values()).flatMap((workbookTables) => Array.from(workbookTables.values()));
538
+ const resourceKeys = new Set;
539
+ for (const table of [...oldTables, ...newTables]) {
540
+ resourceKeys.add(getTableResourceKey({
541
+ workbookName: table.workbookName,
542
+ tableName: table.name
543
+ }));
544
+ }
545
+ this.tableManager.resetTables(tables);
546
+ this.emitMutation({
547
+ touchedCells: mergeTouchedCells(buildTableTouchedCells(this.workbookManager, oldTables), buildTableTouchedCells(this.workbookManager, newTables)),
548
+ tableContextChangedCells: buildTableContextChangedCells(this.workbookManager, [...oldTables, ...newTables]),
549
+ resourceKeys: Array.from(resourceKeys)
550
+ });
447
551
  });
448
552
  }
449
553
  getTables(workbookName) {
@@ -453,14 +557,18 @@ class FormulaEngine {
453
557
  return this.tableManager.isCellInTable(cellAddress);
454
558
  }
455
559
  addConditionalStyle(style) {
456
- this.styleManager.addConditionalStyle(style);
457
- this.emitUpdate();
560
+ return this.withUndoRedoCheckpoint(() => {
561
+ this.styleManager.addConditionalStyle(style);
562
+ this.emitUpdate();
563
+ });
458
564
  }
459
565
  removeConditionalStyle(workbookName, index) {
460
- const removed = this.styleManager.removeConditionalStyle(workbookName, index);
461
- if (removed) {
462
- this.emitUpdate();
463
- }
566
+ return this.withUndoRedoCheckpoint(() => {
567
+ const removed = this.styleManager.removeConditionalStyle(workbookName, index);
568
+ if (removed) {
569
+ this.emitUpdate();
570
+ }
571
+ });
464
572
  }
465
573
  getConditionalStyleCount(workbookName) {
466
574
  const allStyles = this.styleManager.getAllConditionalStyles();
@@ -479,14 +587,18 @@ class FormulaEngine {
479
587
  return this.styleManager.getAllConditionalStyles();
480
588
  }
481
589
  addCellStyle(style) {
482
- this.styleManager.addCellStyle(style);
483
- this.emitUpdate();
590
+ return this.withUndoRedoCheckpoint(() => {
591
+ this.styleManager.addCellStyle(style);
592
+ this.emitUpdate();
593
+ });
484
594
  }
485
595
  removeCellStyle(workbookName, index) {
486
- const removed = this.styleManager.removeCellStyle(workbookName, index);
487
- if (removed) {
488
- this.emitUpdate();
489
- }
596
+ return this.withUndoRedoCheckpoint(() => {
597
+ const removed = this.styleManager.removeCellStyle(workbookName, index);
598
+ if (removed) {
599
+ this.emitUpdate();
600
+ }
601
+ });
490
602
  }
491
603
  getCellStyleCount(workbookName) {
492
604
  const allStyles = this.styleManager.getAllCellStyles();
@@ -499,8 +611,10 @@ class FormulaEngine {
499
611
  return this.styleManager.getStyleForRange(range);
500
612
  }
501
613
  clearCellStyles(range) {
502
- this.styleManager.clearCellStyles(range);
503
- this.emitUpdate();
614
+ return this.withUndoRedoCheckpoint(() => {
615
+ this.styleManager.clearCellStyles(range);
616
+ this.emitUpdate();
617
+ });
504
618
  }
505
619
  getTopLeftCell(cells) {
506
620
  let topLeft = cells[0];
@@ -530,100 +644,106 @@ class FormulaEngine {
530
644
  return this.dedupeAddresses(options.cut ? [...source, ...targetCells] : targetCells);
531
645
  }
532
646
  pasteCells(source, target, options) {
533
- if (source.length === 0) {
534
- return;
535
- }
536
- const touchedAddresses = this.getPasteTouchedAddresses(source, target, options);
537
- const before = captureCellContents(this.workbookManager, touchedAddresses);
538
- this.copyManager.pasteCells(source, target, options);
539
- const after = captureCellContents(this.workbookManager, touchedAddresses);
540
- this.emitMutation({
541
- touchedCells: buildTouchedCells(touchedAddresses.map((address) => ({
542
- address,
543
- before: before.get(getMutationAddressKey(address)),
544
- after: after.get(getMutationAddressKey(address))
545
- }))),
546
- resourceKeys: []
647
+ return this.withUndoRedoCheckpoint(() => {
648
+ if (source.length === 0) {
649
+ return;
650
+ }
651
+ const touchedAddresses = this.getPasteTouchedAddresses(source, target, options);
652
+ const before = captureCellContents(this.workbookManager, touchedAddresses);
653
+ this.copyManager.pasteCells(source, target, options);
654
+ const after = captureCellContents(this.workbookManager, touchedAddresses);
655
+ this.emitMutation({
656
+ touchedCells: buildTouchedCells(touchedAddresses.map((address) => ({
657
+ address,
658
+ before: before.get(getMutationAddressKey(address)),
659
+ after: after.get(getMutationAddressKey(address))
660
+ }))),
661
+ resourceKeys: []
662
+ });
547
663
  });
548
664
  }
549
665
  fillAreas(seedRange, targetRanges, options) {
550
- const touchedAddresses = this.dedupeAddresses([
551
- ...targetRanges.flatMap((targetRange) => getFiniteRangeAddresses(targetRange)),
552
- ...options.cut ? getFiniteRangeAddresses(seedRange) : []
553
- ]);
554
- const before = captureCellContents(this.workbookManager, touchedAddresses);
555
- this.copyManager.fillAreas(seedRange, targetRanges, options);
556
- const after = captureCellContents(this.workbookManager, touchedAddresses);
557
- this.emitMutation({
558
- touchedCells: buildTouchedCells(touchedAddresses.map((address) => ({
559
- address,
560
- before: before.get(getMutationAddressKey(address)),
561
- after: after.get(getMutationAddressKey(address))
562
- }))),
563
- resourceKeys: []
666
+ return this.withUndoRedoCheckpoint(() => {
667
+ const touchedAddresses = this.dedupeAddresses([
668
+ ...targetRanges.flatMap((targetRange) => getFiniteRangeAddresses(targetRange)),
669
+ ...options.cut ? getFiniteRangeAddresses(seedRange) : []
670
+ ]);
671
+ const before = captureCellContents(this.workbookManager, touchedAddresses);
672
+ this.copyManager.fillAreas(seedRange, targetRanges, options);
673
+ const after = captureCellContents(this.workbookManager, touchedAddresses);
674
+ this.emitMutation({
675
+ touchedCells: buildTouchedCells(touchedAddresses.map((address) => ({
676
+ address,
677
+ before: before.get(getMutationAddressKey(address)),
678
+ after: after.get(getMutationAddressKey(address))
679
+ }))),
680
+ resourceKeys: []
681
+ });
564
682
  });
565
683
  }
566
684
  smartPaste(sourceCells, pasteSelection, options) {
567
- if (sourceCells.length === 0) {
568
- return;
569
- }
570
- if (options.cut === true) {
571
- for (const area of pasteSelection.areas) {
572
- const target = {
573
- workbookName: pasteSelection.workbookName,
574
- sheetName: pasteSelection.sheetName,
575
- colIndex: area.start.col,
576
- rowIndex: area.start.row
577
- };
578
- this.pasteCells(sourceCells, target, options);
685
+ return this.withUndoRedoCheckpoint(() => {
686
+ if (sourceCells.length === 0) {
687
+ return;
579
688
  }
580
- return;
581
- }
582
- const sourceBounds = this.getBoundsFromCells(sourceCells);
583
- const sourceWidth = sourceBounds.maxCol - sourceBounds.minCol + 1;
584
- const sourceHeight = sourceBounds.maxRow - sourceBounds.minRow + 1;
585
- const seedRange = {
586
- workbookName: sourceCells[0].workbookName,
587
- sheetName: sourceCells[0].sheetName,
588
- range: {
589
- start: { col: sourceBounds.minCol, row: sourceBounds.minRow },
590
- end: {
591
- col: { type: "number", value: sourceBounds.maxCol },
592
- row: { type: "number", value: sourceBounds.maxRow }
689
+ if (options.cut === true) {
690
+ for (const area of pasteSelection.areas) {
691
+ const target = {
692
+ workbookName: pasteSelection.workbookName,
693
+ sheetName: pasteSelection.sheetName,
694
+ colIndex: area.start.col,
695
+ rowIndex: area.start.row
696
+ };
697
+ this.pasteCells(sourceCells, target, options);
593
698
  }
699
+ return;
594
700
  }
595
- };
596
- for (const area of pasteSelection.areas) {
597
- const pasteStartCol = area.start.col;
598
- const pasteStartRow = area.start.row;
599
- const pasteEndCol = area.end.col.type === "number" ? area.end.col.value : pasteStartCol;
600
- const pasteEndRow = area.end.row.type === "number" ? area.end.row.value : pasteStartRow;
601
- const pasteWidth = pasteEndCol - pasteStartCol + 1;
602
- const pasteHeight = pasteEndRow - pasteStartRow + 1;
603
- const shouldFill = pasteWidth > sourceWidth || pasteHeight > sourceHeight;
604
- if (shouldFill) {
605
- const targetRange = {
606
- workbookName: pasteSelection.workbookName,
607
- sheetName: pasteSelection.sheetName,
608
- range: {
609
- start: { col: pasteStartCol, row: pasteStartRow },
610
- end: {
611
- col: { type: "number", value: pasteEndCol },
612
- row: { type: "number", value: pasteEndRow }
613
- }
701
+ const sourceBounds = this.getBoundsFromCells(sourceCells);
702
+ const sourceWidth = sourceBounds.maxCol - sourceBounds.minCol + 1;
703
+ const sourceHeight = sourceBounds.maxRow - sourceBounds.minRow + 1;
704
+ const seedRange = {
705
+ workbookName: sourceCells[0].workbookName,
706
+ sheetName: sourceCells[0].sheetName,
707
+ range: {
708
+ start: { col: sourceBounds.minCol, row: sourceBounds.minRow },
709
+ end: {
710
+ col: { type: "number", value: sourceBounds.maxCol },
711
+ row: { type: "number", value: sourceBounds.maxRow }
614
712
  }
615
- };
616
- this.fillAreas(seedRange, [targetRange], options);
617
- } else {
618
- const target = {
619
- workbookName: pasteSelection.workbookName,
620
- sheetName: pasteSelection.sheetName,
621
- colIndex: pasteStartCol,
622
- rowIndex: pasteStartRow
623
- };
624
- this.pasteCells(sourceCells, target, options);
713
+ }
714
+ };
715
+ for (const area of pasteSelection.areas) {
716
+ const pasteStartCol = area.start.col;
717
+ const pasteStartRow = area.start.row;
718
+ const pasteEndCol = area.end.col.type === "number" ? area.end.col.value : pasteStartCol;
719
+ const pasteEndRow = area.end.row.type === "number" ? area.end.row.value : pasteStartRow;
720
+ const pasteWidth = pasteEndCol - pasteStartCol + 1;
721
+ const pasteHeight = pasteEndRow - pasteStartRow + 1;
722
+ const shouldFill = pasteWidth > sourceWidth || pasteHeight > sourceHeight;
723
+ if (shouldFill) {
724
+ const targetRange = {
725
+ workbookName: pasteSelection.workbookName,
726
+ sheetName: pasteSelection.sheetName,
727
+ range: {
728
+ start: { col: pasteStartCol, row: pasteStartRow },
729
+ end: {
730
+ col: { type: "number", value: pasteEndCol },
731
+ row: { type: "number", value: pasteEndRow }
732
+ }
733
+ }
734
+ };
735
+ this.fillAreas(seedRange, [targetRange], options);
736
+ } else {
737
+ const target = {
738
+ workbookName: pasteSelection.workbookName,
739
+ sheetName: pasteSelection.sheetName,
740
+ colIndex: pasteStartCol,
741
+ rowIndex: pasteStartRow
742
+ };
743
+ this.pasteCells(sourceCells, target, options);
744
+ }
625
745
  }
626
- }
746
+ });
627
747
  }
628
748
  getBoundsFromCells(cells) {
629
749
  if (cells.length === 0) {
@@ -642,72 +762,84 @@ class FormulaEngine {
642
762
  return { minCol, minRow, maxCol, maxRow };
643
763
  }
644
764
  moveCell(source, target) {
645
- this.pasteCells([source], target, {
646
- cut: true,
647
- type: "formula",
648
- include: "all"
765
+ return this.withUndoRedoCheckpoint(() => {
766
+ this.pasteCells([source], target, {
767
+ cut: true,
768
+ type: "formula",
769
+ include: "all"
770
+ });
649
771
  });
650
772
  }
651
773
  moveRange(sourceRange, target) {
652
- const cells = this.copyManager.expandRangeToCells(sourceRange);
653
- this.pasteCells(cells, target, {
654
- cut: true,
655
- type: "formula",
656
- include: "all"
774
+ return this.withUndoRedoCheckpoint(() => {
775
+ const cells = this.copyManager.expandRangeToCells(sourceRange);
776
+ this.pasteCells(cells, target, {
777
+ cut: true,
778
+ type: "formula",
779
+ include: "all"
780
+ });
657
781
  });
658
782
  }
659
783
  addSheet(opts) {
660
- const sheet = this.workbookManager.addSheet(opts);
661
- this.namedExpressionManager.addSheet(opts);
662
- this.emitMutation({
663
- touchedCells: [],
664
- resourceKeys: [getSheetResourceKey(opts)]
784
+ return this.withUndoRedoCheckpoint(() => {
785
+ const sheet = this.workbookManager.addSheet(opts);
786
+ this.namedExpressionManager.addSheet(opts);
787
+ this.emitMutation({
788
+ touchedCells: [],
789
+ resourceKeys: [getSheetResourceKey(opts)]
790
+ });
791
+ return sheet;
665
792
  });
666
- return sheet;
667
793
  }
668
794
  createSheet(opts) {
669
- const sheetName = opts.sheetName ?? this.workbookManager.getNextAvailableSheetName(opts.workbookName, opts.baseName);
670
- return this.addSheet({
671
- workbookName: opts.workbookName,
672
- sheetName
795
+ return this.withUndoRedoCheckpoint(() => {
796
+ const sheetName = opts.sheetName ?? this.workbookManager.getNextAvailableSheetName(opts.workbookName, opts.baseName);
797
+ return this.addSheet({
798
+ workbookName: opts.workbookName,
799
+ sheetName
800
+ });
673
801
  });
674
802
  }
675
803
  removeSheet(opts) {
676
- const resourceKeys = this.getSheetResourceKeys(opts);
677
- this.workbookManager.removeSheet(opts);
678
- this.namedExpressionManager.removeSheet(opts);
679
- this.tableManager.removeSheet(opts);
680
- this.styleManager.removeSheetStyles(opts.workbookName, opts.sheetName);
681
- this.rangeMetadataManager.removeSheetRangeMetadata(opts.workbookName, opts.sheetName);
682
- this.referenceManager.invalidateSheet(opts.workbookName, opts.sheetName);
683
- this.emitMutation({
684
- touchedCells: [],
685
- resourceKeys,
686
- removedScopes: [{ type: "sheet", ...opts }]
804
+ return this.withUndoRedoCheckpoint(() => {
805
+ const resourceKeys = this.getSheetResourceKeys(opts);
806
+ this.workbookManager.removeSheet(opts);
807
+ this.namedExpressionManager.removeSheet(opts);
808
+ this.tableManager.removeSheet(opts);
809
+ this.styleManager.removeSheetStyles(opts.workbookName, opts.sheetName);
810
+ this.rangeMetadataManager.removeSheetRangeMetadata(opts.workbookName, opts.sheetName);
811
+ this.referenceManager.invalidateSheet(opts.workbookName, opts.sheetName);
812
+ this.emitMutation({
813
+ touchedCells: [],
814
+ resourceKeys,
815
+ removedScopes: [{ type: "sheet", ...opts }]
816
+ });
687
817
  });
688
818
  }
689
819
  renameSheet(opts) {
690
- const oldResourceKeys = this.getSheetResourceKeys(opts);
691
- this.workbookManager.renameSheet(opts);
692
- this.namedExpressionManager.renameSheet(opts);
693
- this.tableManager.updateTablesForSheetRename(opts);
694
- this.styleManager.updateSheetName(opts.workbookName, opts.sheetName, opts.newSheetName);
695
- this.rangeMetadataManager.updateSheetName(opts.workbookName, opts.sheetName, opts.newSheetName);
696
- const changedCells = this.workbookManager.updateAllFormulas((formula) => renameSheetInFormula({
697
- formula,
698
- oldSheetName: opts.sheetName,
699
- newSheetName: opts.newSheetName
700
- }));
701
- this.referenceManager.updateSheetName(opts.workbookName, opts.sheetName, opts.newSheetName);
702
- this.emitMutation({
703
- touchedCells: buildFormulaTouchedCells(changedCells),
704
- resourceKeys: Array.from(new Set([
705
- ...oldResourceKeys,
706
- ...this.getSheetResourceKeys({
707
- workbookName: opts.workbookName,
708
- sheetName: opts.newSheetName
709
- })
710
- ]))
820
+ return this.withUndoRedoCheckpoint(() => {
821
+ const oldResourceKeys = this.getSheetResourceKeys(opts);
822
+ this.workbookManager.renameSheet(opts);
823
+ this.namedExpressionManager.renameSheet(opts);
824
+ this.tableManager.updateTablesForSheetRename(opts);
825
+ this.styleManager.updateSheetName(opts.workbookName, opts.sheetName, opts.newSheetName);
826
+ this.rangeMetadataManager.updateSheetName(opts.workbookName, opts.sheetName, opts.newSheetName);
827
+ const changedCells = this.workbookManager.updateAllFormulas((formula) => renameSheetInFormula({
828
+ formula,
829
+ oldSheetName: opts.sheetName,
830
+ newSheetName: opts.newSheetName
831
+ }));
832
+ this.referenceManager.updateSheetName(opts.workbookName, opts.sheetName, opts.newSheetName);
833
+ this.emitMutation({
834
+ touchedCells: buildFormulaTouchedCells(changedCells),
835
+ resourceKeys: Array.from(new Set([
836
+ ...oldResourceKeys,
837
+ ...this.getSheetResourceKeys({
838
+ workbookName: opts.workbookName,
839
+ sheetName: opts.newSheetName
840
+ })
841
+ ]))
842
+ });
711
843
  });
712
844
  }
713
845
  hasSheet(opts) {
@@ -738,239 +870,259 @@ class FormulaEngine {
738
870
  return this.workbookManager.search(query, options);
739
871
  }
740
872
  replace(query, replacement, target, options) {
741
- const prepared = this.workbookManager.prepareReplace(query, replacement, target, options);
742
- this.workbookManager.setCellContent(prepared.address, prepared.afterContent);
743
- this.emitMutation({
744
- touchedCells: buildTouchedCells([
745
- {
746
- address: prepared.address,
747
- before: prepared.beforeContent,
748
- after: prepared.afterContent
749
- }
750
- ]),
751
- resourceKeys: []
873
+ return this.withUndoRedoCheckpoint(() => {
874
+ const prepared = this.workbookManager.prepareReplace(query, replacement, target, options);
875
+ this.workbookManager.setCellContent(prepared.address, prepared.afterContent);
876
+ this.emitMutation({
877
+ touchedCells: buildTouchedCells([
878
+ {
879
+ address: prepared.address,
880
+ before: prepared.beforeContent,
881
+ after: prepared.afterContent
882
+ }
883
+ ]),
884
+ resourceKeys: []
885
+ });
886
+ return prepared.change;
752
887
  });
753
- return prepared.change;
754
888
  }
755
889
  replaceAll(query, replacement, options) {
756
- const preparedReplacements = this.workbookManager.prepareReplaceAll(query, replacement, options);
757
- if (preparedReplacements.length === 0) {
758
- return [];
759
- }
760
- for (const prepared of preparedReplacements) {
761
- this.workbookManager.setCellContent(prepared.address, prepared.afterContent);
762
- }
763
- this.emitMutation({
764
- touchedCells: buildTouchedCells(preparedReplacements.map((prepared) => ({
765
- address: prepared.address,
766
- before: prepared.beforeContent,
767
- after: prepared.afterContent
768
- }))),
769
- resourceKeys: []
890
+ return this.withUndoRedoCheckpoint(() => {
891
+ const preparedReplacements = this.workbookManager.prepareReplaceAll(query, replacement, options);
892
+ if (preparedReplacements.length === 0) {
893
+ return [];
894
+ }
895
+ for (const prepared of preparedReplacements) {
896
+ this.workbookManager.setCellContent(prepared.address, prepared.afterContent);
897
+ }
898
+ this.emitMutation({
899
+ touchedCells: buildTouchedCells(preparedReplacements.map((prepared) => ({
900
+ address: prepared.address,
901
+ before: prepared.beforeContent,
902
+ after: prepared.afterContent
903
+ }))),
904
+ resourceKeys: []
905
+ });
906
+ return preparedReplacements.flatMap((prepared) => prepared.changes);
770
907
  });
771
- return preparedReplacements.flatMap((prepared) => prepared.changes);
772
908
  }
773
909
  addWorkbook(workbookName) {
774
- this.workbookManager.addWorkbook(workbookName);
775
- this.namedExpressionManager.addWorkbook(workbookName);
776
- this.tableManager.addWorkbook(workbookName);
777
- this.emitMutation({
778
- touchedCells: [],
779
- resourceKeys: [getWorkbookResourceKey(workbookName)]
910
+ return this.withUndoRedoCheckpoint(() => {
911
+ this.workbookManager.addWorkbook(workbookName);
912
+ this.namedExpressionManager.addWorkbook(workbookName);
913
+ this.tableManager.addWorkbook(workbookName);
914
+ this.emitMutation({
915
+ touchedCells: [],
916
+ resourceKeys: [getWorkbookResourceKey(workbookName)]
917
+ });
780
918
  });
781
919
  }
782
920
  removeWorkbook(workbookName) {
783
- const resourceKeys = this.getWorkbookResourceKeys(workbookName);
784
- this.workbookManager.removeWorkbook(workbookName);
785
- this.namedExpressionManager.removeWorkbook(workbookName);
786
- this.tableManager.removeWorkbook(workbookName);
787
- this.styleManager.removeWorkbookStyles(workbookName);
788
- this.rangeMetadataManager.removeWorkbookRangeMetadata(workbookName);
789
- this.referenceManager.invalidateWorkbook(workbookName);
790
- this.emitMutation({
791
- touchedCells: [],
792
- resourceKeys,
793
- removedScopes: [{ type: "workbook", workbookName }]
921
+ return this.withUndoRedoCheckpoint(() => {
922
+ const resourceKeys = this.getWorkbookResourceKeys(workbookName);
923
+ this.workbookManager.removeWorkbook(workbookName);
924
+ this.namedExpressionManager.removeWorkbook(workbookName);
925
+ this.tableManager.removeWorkbook(workbookName);
926
+ this.styleManager.removeWorkbookStyles(workbookName);
927
+ this.rangeMetadataManager.removeWorkbookRangeMetadata(workbookName);
928
+ this.referenceManager.invalidateWorkbook(workbookName);
929
+ this.emitMutation({
930
+ touchedCells: [],
931
+ resourceKeys,
932
+ removedScopes: [{ type: "workbook", workbookName }]
933
+ });
794
934
  });
795
935
  }
796
936
  hasWorkbook(workbookName) {
797
937
  return this.workbookManager.getWorkbooks().has(workbookName);
798
938
  }
799
939
  cloneWorkbook(fromWorkbookName, toWorkbookName) {
800
- const sourceWorkbook = this.workbookManager.getWorkbooks().get(fromWorkbookName);
801
- if (!sourceWorkbook) {
802
- throw new Error(`Source workbook "${fromWorkbookName}" not found`);
803
- }
804
- if (this.workbookManager.getWorkbooks().has(toWorkbookName)) {
805
- throw new Error(`Target workbook "${toWorkbookName}" already exists`);
806
- }
807
- this.workbookManager.addWorkbook(toWorkbookName);
808
- this.namedExpressionManager.addWorkbook(toWorkbookName);
809
- this.tableManager.addWorkbook(toWorkbookName);
810
- for (const [sheetName, sheet] of sourceWorkbook.sheets) {
811
- this.workbookManager.addSheet({
812
- workbookName: toWorkbookName,
813
- sheetName
814
- });
815
- this.namedExpressionManager.addSheet({
816
- workbookName: toWorkbookName,
817
- sheetName
818
- });
819
- this.workbookManager.setSheetContent({ workbookName: toWorkbookName, sheetName }, new Map(sheet.content));
820
- const targetSheet = this.workbookManager.getSheet({
821
- workbookName: toWorkbookName,
822
- sheetName
823
- });
824
- if (targetSheet) {
825
- targetSheet.metadata = new Map(sheet.metadata);
826
- if (sheet.sheetMetadata !== undefined) {
827
- targetSheet.sheetMetadata = structuredClone(sheet.sheetMetadata);
828
- }
940
+ return this.withUndoRedoCheckpoint(() => {
941
+ const sourceWorkbook = this.workbookManager.getWorkbooks().get(fromWorkbookName);
942
+ if (!sourceWorkbook) {
943
+ throw new Error(`Source workbook "${fromWorkbookName}" not found`);
829
944
  }
830
- }
831
- const targetWorkbook = this.workbookManager.getWorkbooks().get(toWorkbookName);
832
- if (targetWorkbook && sourceWorkbook.workbookMetadata !== undefined) {
833
- targetWorkbook.workbookMetadata = structuredClone(sourceWorkbook.workbookMetadata);
834
- }
835
- const namedExpressions = this.namedExpressionManager.getNamedExpressions();
836
- const sourceWorkbookExpressions = namedExpressions.workbookExpressions.get(fromWorkbookName);
837
- if (sourceWorkbookExpressions) {
838
- for (const [expressionName, expression] of sourceWorkbookExpressions) {
839
- this.namedExpressionManager.addNamedExpression({
840
- expressionName,
841
- expression: expression.expression,
842
- workbookName: toWorkbookName
945
+ if (this.workbookManager.getWorkbooks().has(toWorkbookName)) {
946
+ throw new Error(`Target workbook "${toWorkbookName}" already exists`);
947
+ }
948
+ this.workbookManager.addWorkbook(toWorkbookName);
949
+ this.namedExpressionManager.addWorkbook(toWorkbookName);
950
+ this.tableManager.addWorkbook(toWorkbookName);
951
+ for (const [sheetName, sheet] of sourceWorkbook.sheets) {
952
+ this.workbookManager.addSheet({
953
+ workbookName: toWorkbookName,
954
+ sheetName
955
+ });
956
+ this.namedExpressionManager.addSheet({
957
+ workbookName: toWorkbookName,
958
+ sheetName
959
+ });
960
+ this.workbookManager.setSheetContent({ workbookName: toWorkbookName, sheetName }, new Map(sheet.content));
961
+ const targetSheet = this.workbookManager.getSheet({
962
+ workbookName: toWorkbookName,
963
+ sheetName
843
964
  });
965
+ if (targetSheet) {
966
+ targetSheet.metadata = new Map(sheet.metadata);
967
+ if (sheet.sheetMetadata !== undefined) {
968
+ targetSheet.sheetMetadata = structuredClone(sheet.sheetMetadata);
969
+ }
970
+ }
844
971
  }
845
- }
846
- const sourceSheetExpressions = namedExpressions.sheetExpressions.get(fromWorkbookName);
847
- if (sourceSheetExpressions) {
848
- for (const [sheetName, expressions] of sourceSheetExpressions) {
849
- for (const [expressionName, expression] of expressions) {
972
+ const targetWorkbook = this.workbookManager.getWorkbooks().get(toWorkbookName);
973
+ if (targetWorkbook && sourceWorkbook.workbookMetadata !== undefined) {
974
+ targetWorkbook.workbookMetadata = structuredClone(sourceWorkbook.workbookMetadata);
975
+ }
976
+ const namedExpressions = this.namedExpressionManager.getNamedExpressions();
977
+ const sourceWorkbookExpressions = namedExpressions.workbookExpressions.get(fromWorkbookName);
978
+ if (sourceWorkbookExpressions) {
979
+ for (const [expressionName, expression] of sourceWorkbookExpressions) {
850
980
  this.namedExpressionManager.addNamedExpression({
851
981
  expressionName,
852
982
  expression: expression.expression,
853
- workbookName: toWorkbookName,
854
- sheetName
983
+ workbookName: toWorkbookName
855
984
  });
856
985
  }
857
986
  }
858
- }
859
- const sourceTables = this.tableManager.tables.get(fromWorkbookName);
860
- if (sourceTables) {
861
- for (const [tableName] of sourceTables) {
862
- this.tableManager.copyTable({ workbookName: fromWorkbookName, tableName }, { workbookName: toWorkbookName, tableName });
987
+ const sourceSheetExpressions = namedExpressions.sheetExpressions.get(fromWorkbookName);
988
+ if (sourceSheetExpressions) {
989
+ for (const [sheetName, expressions] of sourceSheetExpressions) {
990
+ for (const [expressionName, expression] of expressions) {
991
+ this.namedExpressionManager.addNamedExpression({
992
+ expressionName,
993
+ expression: expression.expression,
994
+ workbookName: toWorkbookName,
995
+ sheetName
996
+ });
997
+ }
998
+ }
863
999
  }
864
- }
865
- for (const style of this.styleManager.getAllConditionalStyles()) {
866
- if (style.areas.some((area) => area.workbookName === fromWorkbookName)) {
867
- this.styleManager.addConditionalStyle({
868
- ...style,
869
- areas: style.areas.map((area) => area.workbookName === fromWorkbookName ? { ...area, workbookName: toWorkbookName } : area)
870
- });
1000
+ const sourceTables = this.tableManager.tables.get(fromWorkbookName);
1001
+ if (sourceTables) {
1002
+ for (const [tableName] of sourceTables) {
1003
+ this.tableManager.copyTable({ workbookName: fromWorkbookName, tableName }, { workbookName: toWorkbookName, tableName });
1004
+ }
871
1005
  }
872
- }
873
- for (const style of this.styleManager.getAllCellStyles()) {
874
- if (style.areas.some((area) => area.workbookName === fromWorkbookName)) {
875
- this.styleManager.addCellStyle({
876
- ...style,
877
- areas: style.areas.map((area) => area.workbookName === fromWorkbookName ? { ...area, workbookName: toWorkbookName } : area)
878
- });
1006
+ for (const style of this.styleManager.getAllConditionalStyles()) {
1007
+ if (style.areas.some((area) => area.workbookName === fromWorkbookName)) {
1008
+ this.styleManager.addConditionalStyle({
1009
+ ...style,
1010
+ areas: style.areas.map((area) => area.workbookName === fromWorkbookName ? { ...area, workbookName: toWorkbookName } : area)
1011
+ });
1012
+ }
879
1013
  }
880
- }
881
- for (const metadata of this.rangeMetadataManager.getAllRangeMetadata()) {
882
- if (metadata.areas.some((area) => area.workbookName === fromWorkbookName)) {
883
- this.rangeMetadataManager.addRangeMetadata({
884
- metadata: metadata.metadata,
885
- areas: metadata.areas.map((area) => area.workbookName === fromWorkbookName ? { ...area, workbookName: toWorkbookName } : area)
886
- });
1014
+ for (const style of this.styleManager.getAllCellStyles()) {
1015
+ if (style.areas.some((area) => area.workbookName === fromWorkbookName)) {
1016
+ this.styleManager.addCellStyle({
1017
+ ...style,
1018
+ areas: style.areas.map((area) => area.workbookName === fromWorkbookName ? { ...area, workbookName: toWorkbookName } : area)
1019
+ });
1020
+ }
887
1021
  }
888
- }
889
- this.workbookManager.updateFormulasForWorkbook(toWorkbookName, (formula) => renameWorkbookInFormula({
890
- formula,
891
- oldWorkbookName: fromWorkbookName,
892
- newWorkbookName: toWorkbookName
893
- }));
894
- this.emitMutation({
895
- touchedCells: [],
896
- resourceKeys: [getWorkbookResourceKey(toWorkbookName)]
1022
+ for (const metadata of this.rangeMetadataManager.getAllRangeMetadata()) {
1023
+ if (metadata.areas.some((area) => area.workbookName === fromWorkbookName)) {
1024
+ this.rangeMetadataManager.addRangeMetadata({
1025
+ metadata: metadata.metadata,
1026
+ areas: metadata.areas.map((area) => area.workbookName === fromWorkbookName ? { ...area, workbookName: toWorkbookName } : area)
1027
+ });
1028
+ }
1029
+ }
1030
+ this.workbookManager.updateFormulasForWorkbook(toWorkbookName, (formula) => renameWorkbookInFormula({
1031
+ formula,
1032
+ oldWorkbookName: fromWorkbookName,
1033
+ newWorkbookName: toWorkbookName
1034
+ }));
1035
+ this.emitMutation({
1036
+ touchedCells: [],
1037
+ resourceKeys: [getWorkbookResourceKey(toWorkbookName)]
1038
+ });
897
1039
  });
898
1040
  }
899
1041
  renameWorkbook(opts) {
900
- const oldResourceKeys = this.getWorkbookResourceKeys(opts.workbookName);
901
- this.workbookManager.renameWorkbook(opts);
902
- this.namedExpressionManager.renameWorkbook(opts);
903
- this.tableManager.updateTablesForWorkbookRename(opts);
904
- this.styleManager.updateWorkbookName(opts.workbookName, opts.newWorkbookName);
905
- this.rangeMetadataManager.updateWorkbookName(opts.workbookName, opts.newWorkbookName);
906
- const changedCells = this.workbookManager.updateAllFormulas((formula) => renameWorkbookInFormula({
907
- formula,
908
- oldWorkbookName: opts.workbookName,
909
- newWorkbookName: opts.newWorkbookName
910
- }));
911
- this.referenceManager.updateWorkbookName(opts.workbookName, opts.newWorkbookName);
912
- this.emitMutation({
913
- touchedCells: buildFormulaTouchedCells(changedCells),
914
- resourceKeys: Array.from(new Set([
915
- ...oldResourceKeys,
916
- ...this.getWorkbookResourceKeys(opts.newWorkbookName)
917
- ]))
1042
+ return this.withUndoRedoCheckpoint(() => {
1043
+ const oldResourceKeys = this.getWorkbookResourceKeys(opts.workbookName);
1044
+ this.workbookManager.renameWorkbook(opts);
1045
+ this.namedExpressionManager.renameWorkbook(opts);
1046
+ this.tableManager.updateTablesForWorkbookRename(opts);
1047
+ this.styleManager.updateWorkbookName(opts.workbookName, opts.newWorkbookName);
1048
+ this.rangeMetadataManager.updateWorkbookName(opts.workbookName, opts.newWorkbookName);
1049
+ const changedCells = this.workbookManager.updateAllFormulas((formula) => renameWorkbookInFormula({
1050
+ formula,
1051
+ oldWorkbookName: opts.workbookName,
1052
+ newWorkbookName: opts.newWorkbookName
1053
+ }));
1054
+ this.referenceManager.updateWorkbookName(opts.workbookName, opts.newWorkbookName);
1055
+ this.emitMutation({
1056
+ touchedCells: buildFormulaTouchedCells(changedCells),
1057
+ resourceKeys: Array.from(new Set([
1058
+ ...oldResourceKeys,
1059
+ ...this.getWorkbookResourceKeys(opts.newWorkbookName)
1060
+ ]))
1061
+ });
918
1062
  });
919
1063
  }
920
1064
  getWorkbooks() {
921
1065
  return this.workbookManager.getWorkbooks();
922
1066
  }
923
1067
  setSheetContent(opts, content) {
924
- const previousContent = this.getExistingSheetContent(opts);
925
- this.workbookManager.setSheetContent(opts, content);
926
- this.emitMutation({
927
- touchedCells: buildSheetContentTouchedCells(opts, previousContent, content),
928
- resourceKeys: []
1068
+ return this.withUndoRedoCheckpoint(() => {
1069
+ const previousContent = this.getExistingSheetContent(opts);
1070
+ this.workbookManager.setSheetContent(opts, content);
1071
+ this.emitMutation({
1072
+ touchedCells: buildSheetContentTouchedCells(opts, previousContent, content),
1073
+ resourceKeys: []
1074
+ });
929
1075
  });
930
1076
  }
931
1077
  setCellContent(address, content) {
932
- const previousValue = this.workbookManager.getCellContent(address);
933
- this.workbookManager.setCellContent(address, content);
934
- this.emitMutation({
935
- touchedCells: buildTouchedCells([
936
- {
937
- address,
938
- before: previousValue,
939
- after: content
940
- }
941
- ]),
942
- resourceKeys: []
1078
+ return this.withUndoRedoCheckpoint(() => {
1079
+ const previousValue = this.workbookManager.getCellContent(address);
1080
+ this.workbookManager.setCellContent(address, content);
1081
+ this.emitMutation({
1082
+ touchedCells: buildTouchedCells([
1083
+ {
1084
+ address,
1085
+ before: previousValue,
1086
+ after: content
1087
+ }
1088
+ ]),
1089
+ resourceKeys: []
1090
+ });
943
1091
  });
944
1092
  }
945
1093
  autoFill(opts, seedRange, fillRanges, direction) {
946
- const touchedAddresses = this.dedupeAddresses(fillRanges.flatMap((range) => getFiniteRangeAddresses({
947
- workbookName: opts.workbookName,
948
- sheetName: opts.sheetName,
949
- range
950
- })));
951
- const before = captureCellContents(this.workbookManager, touchedAddresses);
952
- this.autoFillManager.fill(opts, seedRange, fillRanges, direction);
953
- const after = captureCellContents(this.workbookManager, touchedAddresses);
954
- this.emitMutation({
955
- touchedCells: buildTouchedCells(touchedAddresses.map((address) => ({
956
- address,
957
- before: before.get(getMutationAddressKey(address)),
958
- after: after.get(getMutationAddressKey(address))
959
- }))),
960
- resourceKeys: []
1094
+ return this.withUndoRedoCheckpoint(() => {
1095
+ const touchedAddresses = this.dedupeAddresses(fillRanges.flatMap((range) => getFiniteRangeAddresses({
1096
+ workbookName: opts.workbookName,
1097
+ sheetName: opts.sheetName,
1098
+ range
1099
+ })));
1100
+ const before = captureCellContents(this.workbookManager, touchedAddresses);
1101
+ this.autoFillManager.fill(opts, seedRange, fillRanges, direction);
1102
+ const after = captureCellContents(this.workbookManager, touchedAddresses);
1103
+ this.emitMutation({
1104
+ touchedCells: buildTouchedCells(touchedAddresses.map((address) => ({
1105
+ address,
1106
+ before: before.get(getMutationAddressKey(address)),
1107
+ after: after.get(getMutationAddressKey(address))
1108
+ }))),
1109
+ resourceKeys: []
1110
+ });
961
1111
  });
962
1112
  }
963
1113
  clearSpreadsheetRange(address) {
964
- const clearedCells = Array.from(this.workbookManager.iterateCellsInRange(address));
965
- const before = captureCellContents(this.workbookManager, clearedCells);
966
- this.workbookManager.clearSpreadsheetRange(address);
967
- this.emitMutation({
968
- touchedCells: buildTouchedCells(clearedCells.map((cellAddress) => ({
969
- address: cellAddress,
970
- before: before.get(getMutationAddressKey(cellAddress)),
971
- after: undefined
972
- }))),
973
- resourceKeys: []
1114
+ return this.withUndoRedoCheckpoint(() => {
1115
+ const clearedCells = Array.from(this.workbookManager.iterateCellsInRange(address));
1116
+ const before = captureCellContents(this.workbookManager, clearedCells);
1117
+ this.workbookManager.clearSpreadsheetRange(address);
1118
+ this.emitMutation({
1119
+ touchedCells: buildTouchedCells(clearedCells.map((cellAddress) => ({
1120
+ address: cellAddress,
1121
+ before: before.get(getMutationAddressKey(cellAddress)),
1122
+ after: undefined
1123
+ }))),
1124
+ resourceKeys: []
1125
+ });
974
1126
  });
975
1127
  }
976
1128
  getState() {
@@ -989,6 +1141,17 @@ class FormulaEngine {
989
1141
  }
990
1142
  buildSerializedSnapshot() {
991
1143
  const evaluationSnapshots = this.dependencyManager.toSnapshot(this.evaluationManager);
1144
+ const historySnapshot = this.buildHistorySnapshot();
1145
+ return {
1146
+ version: ENGINE_SNAPSHOT_VERSION,
1147
+ managers: {
1148
+ ...historySnapshot.managers,
1149
+ dependency: evaluationSnapshots.dependency,
1150
+ cache: evaluationSnapshots.cache
1151
+ }
1152
+ };
1153
+ }
1154
+ buildHistorySnapshot() {
992
1155
  const workbookSnapshot = this.workbookManager.toSnapshot();
993
1156
  return {
994
1157
  version: ENGINE_SNAPSHOT_VERSION,
@@ -998,12 +1161,13 @@ class FormulaEngine {
998
1161
  table: this.tableManager.toSnapshot(),
999
1162
  style: this.styleManager.toSnapshot(),
1000
1163
  rangeMetadata: this.rangeMetadataManager.toSnapshot(),
1001
- reference: this.referenceManager.toSnapshot(),
1002
- dependency: evaluationSnapshots.dependency,
1003
- cache: evaluationSnapshots.cache
1164
+ reference: this.referenceManager.toSnapshot()
1004
1165
  }
1005
1166
  };
1006
1167
  }
1168
+ serializeHistorySnapshot() {
1169
+ return serialize(this.buildHistorySnapshot());
1170
+ }
1007
1171
  buildNamedExpressionSnapshot(workbookSnapshot) {
1008
1172
  const namedExpressions = this.namedExpressionManager.toSnapshot();
1009
1173
  const workbookExpressions = new Map;
@@ -1027,14 +1191,15 @@ class FormulaEngine {
1027
1191
  serializeEngine() {
1028
1192
  return serialize(this.buildSerializedSnapshot());
1029
1193
  }
1030
- resetToSerializedEngine(data) {
1031
- const deserialized = deserialize(data);
1032
- if (!deserialized || typeof deserialized !== "object" || !("version" in deserialized) || deserialized.version !== ENGINE_SNAPSHOT_VERSION || !deserialized.managers) {
1194
+ assertSupportedSnapshot(snapshot) {
1195
+ if (!snapshot || typeof snapshot !== "object" || !("version" in snapshot) || snapshot.version !== ENGINE_SNAPSHOT_VERSION || !snapshot.managers) {
1033
1196
  throw new Error(`Unsupported serialized engine format. Expected EngineSnapshot version ${ENGINE_SNAPSHOT_VERSION}.`);
1034
1197
  }
1198
+ }
1199
+ restoreDataManagersFromSnapshot(managers) {
1035
1200
  this.namedExpressionManager.clear();
1036
- this.workbookManager.restoreFromSnapshot(deserialized.managers.workbook);
1037
- deserialized.managers.workbook.forEach((workbook) => {
1201
+ this.workbookManager.restoreFromSnapshot(managers.workbook);
1202
+ managers.workbook.forEach((workbook) => {
1038
1203
  this.namedExpressionManager.addWorkbook(workbook.name);
1039
1204
  workbook.sheets.forEach((sheet) => {
1040
1205
  this.namedExpressionManager.addSheet({
@@ -1043,15 +1208,27 @@ class FormulaEngine {
1043
1208
  });
1044
1209
  });
1045
1210
  });
1046
- this.namedExpressionManager.restoreFromSnapshot(deserialized.managers.namedExpression);
1047
- this.tableManager.restoreFromSnapshot(deserialized.managers.table);
1048
- this.styleManager.restoreFromSnapshot(deserialized.managers.style);
1049
- this.rangeMetadataManager.restoreFromSnapshot(deserialized.managers.rangeMetadata);
1050
- this.referenceManager.restoreFromSnapshot(deserialized.managers.reference);
1211
+ this.namedExpressionManager.restoreFromSnapshot(managers.namedExpression);
1212
+ this.tableManager.restoreFromSnapshot(managers.table);
1213
+ this.styleManager.restoreFromSnapshot(managers.style);
1214
+ this.rangeMetadataManager.restoreFromSnapshot(managers.rangeMetadata);
1215
+ this.referenceManager.restoreFromSnapshot(managers.reference);
1216
+ }
1217
+ restoreHistorySnapshot(data) {
1218
+ const deserialized = deserialize(data);
1219
+ this.assertSupportedSnapshot(deserialized);
1220
+ this.restoreDataManagersFromSnapshot(deserialized.managers);
1221
+ this.evaluationManager.clearEvaluationCache();
1222
+ }
1223
+ resetToSerializedEngine(data) {
1224
+ const deserialized = deserialize(data);
1225
+ this.assertSupportedSnapshot(deserialized);
1226
+ this.restoreDataManagersFromSnapshot(deserialized.managers);
1051
1227
  this.dependencyManager.restoreFromSnapshot({
1052
1228
  dependency: deserialized.managers.dependency,
1053
1229
  cache: deserialized.managers.cache
1054
1230
  }, this.evaluationManager);
1231
+ this.clearUndoRedoHistory();
1055
1232
  this.eventManager.emitUpdate();
1056
1233
  }
1057
1234
  }
@@ -1059,4 +1236,4 @@ export {
1059
1236
  FormulaEngine
1060
1237
  };
1061
1238
 
1062
- //# debugId=C4AB97ECFD760E2E64756E2164756E21
1239
+ //# debugId=D1ED50A99BB06C3D64756E2164756E21