From 7917aadcf63703895b311aa86f987d7aecdd20d8 Mon Sep 17 00:00:00 2001 From: Youngteac Hong Date: Fri, 22 Sep 2023 10:21:49 +0900 Subject: [PATCH] Extract history from document --- src/document/document.ts | 83 +++++---------------------- src/document/history.ts | 117 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 70 deletions(-) create mode 100644 src/document/history.ts diff --git a/src/document/document.ts b/src/document/document.ts index 297dfa4ba..6cae1b8b1 100644 --- a/src/document/document.ts +++ b/src/document/document.ts @@ -66,6 +66,7 @@ import { Presence, PresenceChangeType, } from '@yorkie-js-sdk/src/document/presence/presence'; +import { History } from '@yorkie-js-sdk/src/document/history'; /** * `DocumentOptions` are the options to create a new document. @@ -399,8 +400,6 @@ type PathOf = PathOfInternal< Depth >; -export const MaxUndoRedoStackDepth = 50; - /** * `Document` is a CRDT-based data type. We can represent the model * of the application and edit it even while offline. @@ -436,19 +435,14 @@ export class Document { private presences: Map; /** - * `history` manages undo and redo of document. + * `history` is exposed to the user to manage undo/redo operations. */ public history; /** - * `undoStack` stores the history of undo operations. - */ - private undoStack: Array>>; - - /** - * `redoStack` stores the history of redo operations. + * `internalHistory` is used to manage undo/redo operations internally. */ - private redoStack: Array>>; + public internalHistory: History

; /** * `isUpdating` is whether the document is updating or not. It is used to @@ -475,9 +469,7 @@ export class Document { this.presences = new Map(); this.isUpdating = false; - this.undoStack = []; - this.redoStack = []; - + this.internalHistory = new History(); this.history = { canUndo: this.canUndo.bind(this), canRedo: this.canRedo.bind(this), @@ -550,9 +542,9 @@ export class Document { this.localChanges.push(change); if (reverseOps.length > 0) { - this.pushUndo(reverseOps); + this.internalHistory.pushUndo(reverseOps); } - this.clearRedo(); + this.internalHistory.clearRedo(); this.changeID = change.getID(); if (change.hasOperations()) { @@ -1232,41 +1224,14 @@ export class Document { * `canUndo` returns whether there are any operations to undo. */ private canUndo(): boolean { - return this.undoStack.length > 0 && !this.isUpdating; + return this.internalHistory.hasUndo() && !this.isUpdating; } /** * `canRedo` returns whether there are any operations to redo. */ private canRedo(): boolean { - return this.redoStack.length > 0 && !this.isUpdating; - } - - /** - * `pushUndo` pushes new undo operations of a change to undo stack. - */ - private pushUndo(undoOps: Array>): void { - if (this.undoStack.length >= MaxUndoRedoStackDepth) { - this.undoStack.shift(); - } - this.undoStack.push(undoOps); - } - - /** - * `pushRedo` pushes new redo operations of a change to redo stack. - */ - private pushRedo(redoOps: Array>): void { - if (this.redoStack.length >= MaxUndoRedoStackDepth) { - this.redoStack.shift(); - } - this.redoStack.push(redoOps); - } - - /** - * `clearRedo` flushes remaining redo operations. - */ - private clearRedo(): void { - this.redoStack = []; + return this.internalHistory.hasRedo() && !this.isUpdating; } /** @@ -1277,7 +1242,7 @@ export class Document { if (this.isUpdating) { throw new Error('Undo is not allowed during an update'); } - const undoOps = this.undoStack.pop(); + const undoOps = this.internalHistory.popUndo(); if (undoOps === undefined) { throw new Error('There is no operation to be undone'); } @@ -1322,7 +1287,7 @@ export class Document { }); } if (reverseOps.length > 0) { - this.pushRedo(reverseOps); + this.internalHistory.pushRedo(reverseOps); } this.localChanges.push(change); @@ -1359,7 +1324,7 @@ export class Document { throw new Error('Redo is not allowed during an update'); } - const redoOps = this.redoStack.pop(); + const redoOps = this.internalHistory.popRedo(); if (redoOps === undefined) { throw new Error('There is no operation to be redone'); } @@ -1404,7 +1369,7 @@ export class Document { }); } if (reverseOps.length > 0) { - this.pushUndo(reverseOps); + this.internalHistory.pushUndo(reverseOps); } this.localChanges.push(change); @@ -1431,26 +1396,4 @@ export class Document { }); } } - - /** - * `getUndoStackForTest` returns the undo stack for test. - */ - public getUndoStackForTest(): Array> { - return this.undoStack.map((ops) => - ops.map((op) => { - return op instanceof Operation ? op.toTestString() : JSON.stringify(op); - }), - ); - } - - /** - * `getRedoStackForTest` returns the redo stack for test. - */ - public getRedoStackForTest(): Array> { - return this.redoStack.map((ops) => - ops.map((op) => { - return op instanceof Operation ? op.toTestString() : JSON.stringify(op); - }), - ); - } } diff --git a/src/document/history.ts b/src/document/history.ts new file mode 100644 index 000000000..ad99b40bd --- /dev/null +++ b/src/document/history.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2023 The Yorkie Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Operation } from './operation/operation'; +import { Indexable } from './document'; + +/** + * `HistoryOperation` is a type of history operation. + */ +export type HistoryOperation

= + | Operation + | { + type: 'presence'; + value: Partial

; + }; + +/** + * `MaxUndoRedoStackDepth` is the maximum depth of undo/redo stack. + */ +export const MaxUndoRedoStackDepth = 50; + +/** + * `History` is a class that stores the history of the document. + */ +export class History

{ + private undoStack: Array>> = []; + private redoStack: Array>> = []; + + /** + * `hasUndo` returns true if there are undo operations. + */ + public hasUndo(): boolean { + return this.undoStack.length > 0; + } + + /** + * `hasRedo` returns true if there are redo operations. + */ + public hasRedo(): boolean { + return this.redoStack.length > 0; + } + + /** + * `pushUndo` pushes new undo operations of a change to undo stack. + */ + public pushUndo(undoOps: Array>): void { + if (this.undoStack.length >= MaxUndoRedoStackDepth) { + this.undoStack.shift(); + } + this.undoStack.push(undoOps); + } + + /** + * `popUndo` pops the last undo operations of a change from undo stack. + */ + public popUndo(): Array> | undefined { + return this.undoStack.pop(); + } + + /** + * `pushRedo` pushes new redo operations of a change to redo stack. + */ + public pushRedo(redoOps: Array>): void { + if (this.redoStack.length >= MaxUndoRedoStackDepth) { + this.redoStack.shift(); + } + this.redoStack.push(redoOps); + } + + /** + * `popRedo` pops the last redo operations of a change from redo stack. + */ + public popRedo(): Array> | undefined { + return this.redoStack.pop(); + } + + /** + * `clearRedo` flushes remaining redo operations. + */ + public clearRedo(): void { + this.redoStack = []; + } + + /** + * `getUndoStackForTest` returns the undo stack for test. + */ + public getUndoStackForTest(): Array> { + return this.undoStack.map((ops) => + ops.map((op) => { + return op instanceof Operation ? op.toTestString() : JSON.stringify(op); + }), + ); + } + + /** + * `getRedoStackForTest` returns the redo stack for test. + */ + public getRedoStackForTest(): Array> { + return this.redoStack.map((ops) => + ops.map((op) => { + return op instanceof Operation ? op.toTestString() : JSON.stringify(op); + }), + ); + } +}