Skip to content

Commit fc78292

Browse files
committed
feat(history): tree history has size methods
1 parent c0a869b commit fc78292

File tree

3 files changed

+94
-11
lines changed

3 files changed

+94
-11
lines changed

src/api/undo/TreeUndoHistory.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,14 @@ export abstract class TreeUndoHistory implements UndoHistoryBase {
197197
public abstract import(dtoHistory: TreeUndoHistoryDTO, fn: (dtoUndoable: unknown) => Undoable): void;
198198

199199
public abstract get considersEqualCmds(): boolean;
200+
201+
/**
202+
* The number of elements the history contains in all the branches
203+
*/
204+
public abstract size(): number;
205+
206+
/**
207+
* An RX object to observe the number of elements in the history.
208+
*/
209+
public abstract sizeObservable(): Observable<number>;
200210
}

src/impl/undo/TreeUndoHistoryImpl.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ class UndoableTreeNodeDTOImpl implements UndoableTreeNodeDTO {
7474
}
7575

7676
/**
77-
* Produces a tree node from the DTO node. This operates recurcively on
77+
* Produces a tree node from the DTO node. This operates recursively on
7878
* children, so that it converts all the tree node.
7979
* @param dto - The DTO to convert
80-
* @param fn - The convertion method for the undoable.
80+
* @param fn - The conversion method for the undoable.
8181
* @param parent - The parent node of the one to create.
8282
* @returns The created tree node (and its children) and the list of created nodes.
8383
*/
@@ -108,6 +108,8 @@ export class TreeUndoHistoryImpl extends TreeUndoHistory {
108108

109109
private readonly redoPublisher: Subject<Undoable | undefined>;
110110

111+
private readonly sizePublisher: Subject<number>;
112+
111113
public readonly root: UndoableTreeNode;
112114

113115
public readonly considersEqualCmds: boolean;
@@ -147,6 +149,7 @@ export class TreeUndoHistoryImpl extends TreeUndoHistory {
147149
this._currentNode = this.root;
148150
this.undoPublisher = new Subject();
149151
this.redoPublisher = new Subject();
152+
this.sizePublisher = new Subject();
150153
}
151154

152155
public add(undoable: Undoable): void {
@@ -165,6 +168,7 @@ export class TreeUndoHistoryImpl extends TreeUndoHistory {
165168
this.idCounter++;
166169
this.undoPublisher.next(undoable);
167170
this.redoPublisher.next(undefined);
171+
this.sizePublisher.next(this.size());
168172
} else {
169173
this.goTo(equalCmd.id);
170174
}
@@ -182,6 +186,7 @@ export class TreeUndoHistoryImpl extends TreeUndoHistory {
182186
this.idCounter = 0;
183187
this.undoPublisher.next(undefined);
184188
this.redoPublisher.next(undefined);
189+
this.sizePublisher.next(0);
185190
}
186191

187192
public delete(id: number): void {
@@ -213,6 +218,7 @@ export class TreeUndoHistoryImpl extends TreeUndoHistory {
213218
if (node.parent.lastChildUndone === node) {
214219
node.parent.lastChildUndone = undefined;
215220
}
221+
this.sizePublisher.next(this.size());
216222
}
217223

218224
// Cloning the array since 'delete' may alter the children list.
@@ -439,4 +445,18 @@ export class TreeUndoHistoryImpl extends TreeUndoHistory {
439445
this.undoPublisher.next(this.getLastUndo());
440446
this.redoPublisher.next(this.getLastRedo());
441447
}
448+
449+
public override size(): number {
450+
return this.childrenNode(this.root);
451+
}
452+
453+
private childrenNode(node: UndoableTreeNode): number {
454+
return node.children
455+
.map(child => this.childrenNode(child))
456+
.reduce((x, y) => x + y, 0) + node.children.length;
457+
}
458+
459+
public override sizeObservable(): Observable<number> {
460+
return this.sizePublisher;
461+
}
442462
}

test/undo/TreeUndoHistory.test.ts

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,27 @@ describe("using a tree undo history", () => {
5959

6060
test("initial history is empty", () => {
6161
expect(history.undoableNodes).toHaveLength(0);
62+
expect(history.size()).toBe(0);
6263
expect(history.currentNode).toBe(history.root);
6364
});
6465

6566
test("undo does nothing", () => {
6667
history.undo();
67-
expect(history.undoableNodes).toHaveLength(0);
68+
expect(history.size()).toBe(0);
6869
expect(history.currentNode).toBe(history.root);
6970
});
7071

7172
test("go to empty ok", () => {
7273
history.goTo(0);
73-
expect(history.undoableNodes).toHaveLength(0);
74+
expect(history.size()).toBe(0);
7475
expect(history.currentNode).toBe(history.root);
7576
});
7677

7778
test("delete invalid node ok", () => {
7879
history.delete(0);
79-
expect(history.undoableNodes).toHaveLength(0);
80+
expect(history.size()).toBe(0);
8081
expect(history.currentNode).toBe(history.root);
82+
expect(history.size()).toBe(0);
8183
});
8284

8385
test("get last undoable when empty", () => {
@@ -138,7 +140,10 @@ describe("using a tree undo history", () => {
138140
.toBe("-a", {"a": undoable0});
139141
expectObservable(history.redosObservable())
140142
.toBe("-a", {"a": undefined});
143+
expectObservable(history.sizeObservable())
144+
.toBe("-a", {"a": 1});
141145
});
146+
expect(history.size()).toBe(1);
142147
});
143148

144149
test("two adds", () => {
@@ -150,6 +155,28 @@ describe("using a tree undo history", () => {
150155
{"a": undoable0, "b": undoable1});
151156
expectObservable(history.redosObservable()).toBe("-a-b",
152157
{"a": undefined, "b": undefined});
158+
expectObservable(history.sizeObservable()).toBe("-a-b",
159+
{"a": 1, "b": 2});
160+
});
161+
expect(history.size()).toBe(2);
162+
});
163+
164+
test("delete obs size ok", () => {
165+
testScheduler.run(helpers => {
166+
const {cold, expectObservable} = helpers;
167+
cold("-a-b-c-d", {"a": undoable0, "b": undoable1, "c": undoable2, "d": 2})
168+
.subscribe(v => {
169+
// eslint-disable-next-line jest/no-conditional-in-test
170+
if (typeof v === "number") {
171+
history.goTo(v - 1);
172+
history.delete(v);
173+
} else {
174+
history.add(v);
175+
}
176+
});
177+
178+
expectObservable(history.sizeObservable()).toBe("-a-b-c-d",
179+
{"a": 1, "b": 2, "c": 3, "d": 2});
153180
});
154181
});
155182

@@ -165,6 +192,7 @@ describe("using a tree undo history", () => {
165192
expectObservable(history.redosObservable()).toBe("-a-b",
166193
{"a": undefined, "b": undoable0});
167194
});
195+
expect(history.size()).toBe(1);
168196
});
169197

170198
test("one add one undo redo", () => {
@@ -183,6 +211,7 @@ describe("using a tree undo history", () => {
183211
expectObservable(history.redosObservable()).toBe("-a-b-c",
184212
{"a": undefined, "b": undoable0, "c": undefined});
185213
});
214+
expect(history.size()).toBe(1);
186215
});
187216

188217
test("three add then go to", () => {
@@ -202,6 +231,7 @@ describe("using a tree undo history", () => {
202231
expectObservable(history.redosObservable()).toBe("-a-b-c-d",
203232
{"a": undefined, "b": undefined, "c": undefined, "d": undoable1});
204233
});
234+
expect(history.size()).toBe(3);
205235
});
206236

207237
test("two add one undo one new add", () => {
@@ -221,6 +251,7 @@ describe("using a tree undo history", () => {
221251
expectObservable(history.redosObservable()).toBe("-a-b-c-d",
222252
{"a": undefined, "b": undefined, "c": undoable1, "d": undefined});
223253
});
254+
expect(history.size()).toBe(3);
224255
});
225256

226257
test("three add one, undo to root, redo to leaf", () => {
@@ -253,6 +284,7 @@ describe("using a tree undo history", () => {
253284
{"a": undefined, "b": undefined, "c": undefined, "d": undoable0, "e": undefined});
254285
});
255286
expect(res2).toStrictEqual([0, 1, 2]);
287+
expect(history.size()).toBe(3);
256288
});
257289
});
258290

@@ -292,7 +324,7 @@ describe("using a tree undo history", () => {
292324

293325
test("undo works", () => {
294326
history.undo();
295-
expect(history.undoableNodes).toHaveLength(1);
327+
expect(history.size()).toBe(1);
296328
expect(history.undoableNodes[0]).toBeDefined();
297329
expect(history.currentNode).toBe(history.root);
298330
expect(undoable0.undo).toHaveBeenCalledTimes(1);
@@ -402,12 +434,14 @@ describe("using a tree undo history", () => {
402434
expect(history.currentNode).toBe(history.root);
403435
expect(history.currentNode.children).toHaveLength(0);
404436
expect(history.undoableNodes).toHaveLength(0);
437+
expect(history.size()).toBe(0);
405438
});
406439

407440
test("clear then add restarts ID at 0", () => {
408441
history.clear();
409442
history.add(undoable1);
410443
expect(history.undoableNodes[0]?.id).toBe(0);
444+
expect(history.size()).toBe(1);
411445
});
412446

413447
test("go to itself", () => {
@@ -457,19 +491,19 @@ describe("using a tree undo history", () => {
457491

458492
test("delete negative node ID ok", () => {
459493
history.delete(-1);
460-
expect(history.undoableNodes).toHaveLength(1);
494+
expect(history.size()).toBe(1);
461495
expect(history.currentNode.undoable).toBe(undoable0);
462496
});
463497

464498
test("delete invalid ok", () => {
465499
history.delete(1);
466-
expect(history.undoableNodes).toHaveLength(1);
500+
expect(history.size()).toBe(1);
467501
expect(history.currentNode.undoable).toBe(undoable0);
468502
});
469503

470504
test("cannot delete the current branch", () => {
471505
history.delete(0);
472-
expect(history.undoableNodes).toHaveLength(1);
506+
expect(history.size()).toBe(1);
473507
expect(history.currentNode.undoable).toBe(undoable0);
474508
});
475509

@@ -502,7 +536,7 @@ describe("using a tree undo history", () => {
502536
});
503537

504538
expect(history.path).toHaveLength(0);
505-
expect(history.undoableNodes).toHaveLength(1);
539+
expect(history.size()).toBe(1);
506540
expect((history.undoableNodes[0]?.undoable as Undoable4Test).foo).toBe(2);
507541
});
508542

@@ -542,9 +576,12 @@ describe("using a tree undo history", () => {
542576
history.add(undoable1);
543577
});
544578

579+
test("size", () => {
580+
expect(history.size()).toBe(2);
581+
});
582+
545583
test("check structure", () => {
546584
expect(history.currentNode.undoable).toBe(undoable1);
547-
expect(history.undoableNodes).toHaveLength(2);
548585
expect(history.undoableNodes[0]?.parent).toBe(history.root);
549586
expect(history.undoableNodes[1]?.parent).toBe(history.root);
550587
expect(history.undoableNodes[0]?.children).toHaveLength(0);
@@ -556,6 +593,7 @@ describe("using a tree undo history", () => {
556593
history.delete(1);
557594
expect(history.undoableNodes[0]?.undoable).toBe(undoable0);
558595
expect(history.undoableNodes[1]?.undoable).toBeUndefined();
596+
expect(history.size()).toBe(1);
559597
});
560598

561599
test("positions OK", () => {
@@ -611,6 +649,10 @@ describe("using a tree undo history", () => {
611649
history.add(undoable4);
612650
});
613651

652+
test("size", () => {
653+
expect(history.size()).toBe(5);
654+
});
655+
614656
test("export", () => {
615657
const res = history.export(() => ({}));
616658

@@ -733,6 +775,7 @@ describe("using a tree undo history", () => {
733775
history.undo();
734776
history.delete(4);
735777
expect(history.getLastRedo()).toBe(undoable3);
778+
expect(history.size()).toBe(4);
736779
});
737780

738781
test("get last redoable message when moving to 2 and undo", () => {
@@ -779,6 +822,7 @@ describe("using a tree undo history", () => {
779822
test("delete 1", () => {
780823
history.delete(1);
781824

825+
expect(history.size()).toBe(4);
782826
expect(history.currentNode.undoable).toBe(undoable4);
783827
expect(history.undoableNodes[0]?.parent).toBe(history.root);
784828
expect(history.undoableNodes[0]?.children).toHaveLength(1);
@@ -798,6 +842,7 @@ describe("using a tree undo history", () => {
798842
history.goTo(0);
799843
history.delete(2);
800844

845+
expect(history.size()).toBe(2);
801846
expect(history.currentNode.undoable).toBe(undoable0);
802847
expect(history.undoableNodes[0]?.parent).toBe(history.root);
803848
expect(history.undoableNodes[0]?.children).toHaveLength(1);
@@ -813,6 +858,7 @@ describe("using a tree undo history", () => {
813858
history.goTo(1);
814859
history.delete(2);
815860

861+
expect(history.size()).toBe(2);
816862
expect(history.currentNode.undoable).toBe(undoable1);
817863
expect(history.undoableNodes[0]).toBeDefined();
818864
expect(history.undoableNodes[1]).toBeDefined();
@@ -824,6 +870,7 @@ describe("using a tree undo history", () => {
824870
test("delete invalid 5", () => {
825871
history.delete(5);
826872

873+
expect(history.size()).toBe(5);
827874
expect(history.currentNode.undoable).toBe(undoable4);
828875
expect(history.undoableNodes[0]?.children).toHaveLength(2);
829876
expect(history.undoableNodes[1]?.children).toHaveLength(0);
@@ -894,6 +941,10 @@ describe("using a tree undo history", () => {
894941
history.add(undoable14);
895942
});
896943

944+
test("size", () => {
945+
expect(history.size()).toBe(15);
946+
});
947+
897948
test("tree structure is valid", () => {
898949
expect(history.currentNode.undoable).toBe(undoable14);
899950
expect(history.undoableNodes[0]?.children).toHaveLength(2);
@@ -1013,6 +1064,7 @@ describe("using a tree undo history", () => {
10131064

10141065
expect(history.getLastUndo()).toBe(undoableC);
10151066
expect(history.getLastRedo()).toBe(undoableD);
1067+
expect(history.size()).toBe(5);
10161068
});
10171069

10181070
test("does not create a new branch, other config", () => {
@@ -1029,6 +1081,7 @@ describe("using a tree undo history", () => {
10291081

10301082
expect(history.getLastUndo()).toBe(undoableC);
10311083
expect(history.getLastRedo()).toBe(undoableD);
1084+
expect(history.size()).toBe(5);
10321085
});
10331086
});
10341087
});

0 commit comments

Comments
 (0)