From 69d4a5c821123f2b08b69390ab9eaba40b200e84 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 4 Mar 2025 14:41:44 +0100 Subject: [PATCH] [UndoManager] support global undo --- src/utils/UndoManager.js | 24 +++++++++++++----------- tests/undo-redo.tests.js | 13 +++++++++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index a3645cd8..3cc395de 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -39,7 +39,7 @@ export class StackItem { */ const clearUndoManagerStackItem = (tr, um, stackItem) => { iterateDeletedStructs(tr, stackItem.deletions, item => { - if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) { + if (item instanceof Item && um.scope.some(type => type === tr.doc || isParentOf(/** @type {AbstractType} */ (type), item))) { keepItem(item, false) } }) @@ -81,7 +81,7 @@ const popStackItem = (undoManager, stack, eventType) => { } struct = item } - if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { + if (!struct.deleted && scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType} */ (type), /** @type {Item} */ (struct)))) { itemsToDelete.push(struct) } } @@ -89,7 +89,7 @@ const popStackItem = (undoManager, stack, eventType) => { iterateDeletedStructs(transaction, stackItem.deletions, struct => { if ( struct instanceof Item && - scope.some(type => isParentOf(type, struct)) && + scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType} */ (type), struct)) && // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval. !isDeleted(stackItem.insertions, struct.id) ) { @@ -159,7 +159,7 @@ const popStackItem = (undoManager, stack, eventType) => { */ export class UndoManager extends ObservableV2 { /** - * @param {AbstractType|Array>} typeScope Accepts either a single type, or an array of types + * @param {Doc|AbstractType|Array>} typeScope Accepts either a single type, or an array of types * @param {UndoManagerOptions} options */ constructor (typeScope, { @@ -168,11 +168,11 @@ export class UndoManager extends ObservableV2 { deleteFilter = () => true, trackedOrigins = new Set([null]), ignoreRemoteMapChanges = false, - doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope.doc) + doc = /** @type {Doc} */ (array.isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc) } = {}) { super() /** - * @type {Array>} + * @type {Array | Doc>} */ this.scope = [] this.doc = doc @@ -212,7 +212,7 @@ export class UndoManager extends ObservableV2 { // Only track certain transactions if ( !this.captureTransaction(transaction) || - !this.scope.some(type => transaction.changedParentTypes.has(type)) || + !this.scope.some(type => transaction.changedParentTypes.has(/** @type {AbstractType} */ (type)) || type === this.doc) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor))) ) { return @@ -251,7 +251,7 @@ export class UndoManager extends ObservableV2 { } // make sure that deleted structs are not gc'd iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => { - if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { + if (item instanceof Item && this.scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType} */ (type), item))) { keepItem(item, true) } }) @@ -272,13 +272,15 @@ export class UndoManager extends ObservableV2 { } /** - * @param {Array> | AbstractType} ytypes + * @param {Array | Doc> | AbstractType | Doc} ytypes */ addToScope (ytypes) { + const tmpSet = new Set(this.scope) ytypes = array.isArray(ytypes) ? ytypes : [ytypes] ytypes.forEach(ytype => { - if (this.scope.every(yt => yt !== ytype)) { - if (ytype.doc !== this.doc) logging.warn('[yjs#509] Not same Y.Doc') // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509 + if (!tmpSet.has(ytype)) { + tmpSet.add(ytype) + if (ytype instanceof AbstractType ? ytype.doc !== this.doc : ytype !== this.doc) logging.warn('[yjs#509] Not same Y.Doc') // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509 this.scope.push(ytype) } }) diff --git a/tests/undo-redo.tests.js b/tests/undo-redo.tests.js index 2fb2ff2e..df9f9ab3 100644 --- a/tests/undo-redo.tests.js +++ b/tests/undo-redo.tests.js @@ -116,6 +116,19 @@ export const testEmptyTypeScope = _tc => { t.assert(yarray.length === 0) } +/** + * Test case to fix #241 + * @param {t.TestCase} _tc + */ +export const testGlobalScope = _tc => { + const ydoc = new Y.Doc() + const um = new Y.UndoManager(ydoc) + const yarray = ydoc.getArray() + yarray.insert(0, [1]) + um.undo() + t.assert(yarray.length === 0) +} + /** * Test case to fix #241 * @param {t.TestCase} _tc