[UndoManager] support global undo

This commit is contained in:
Kevin Jahns 2025-03-04 14:41:44 +01:00
parent cc9a857441
commit 69d4a5c821
2 changed files with 26 additions and 11 deletions

View File

@ -39,7 +39,7 @@ export class StackItem {
*/ */
const clearUndoManagerStackItem = (tr, um, stackItem) => { const clearUndoManagerStackItem = (tr, um, stackItem) => {
iterateDeletedStructs(tr, stackItem.deletions, item => { 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<any>} */ (type), item))) {
keepItem(item, false) keepItem(item, false)
} }
}) })
@ -81,7 +81,7 @@ const popStackItem = (undoManager, stack, eventType) => {
} }
struct = item 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<any>} */ (type), /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct) itemsToDelete.push(struct)
} }
} }
@ -89,7 +89,7 @@ const popStackItem = (undoManager, stack, eventType) => {
iterateDeletedStructs(transaction, stackItem.deletions, struct => { iterateDeletedStructs(transaction, stackItem.deletions, struct => {
if ( if (
struct instanceof Item && struct instanceof Item &&
scope.some(type => isParentOf(type, struct)) && scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), struct)) &&
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval. // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
!isDeleted(stackItem.insertions, struct.id) !isDeleted(stackItem.insertions, struct.id)
) { ) {
@ -159,7 +159,7 @@ const popStackItem = (undoManager, stack, eventType) => {
*/ */
export class UndoManager extends ObservableV2 { export class UndoManager extends ObservableV2 {
/** /**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types * @param {Doc|AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options * @param {UndoManagerOptions} options
*/ */
constructor (typeScope, { constructor (typeScope, {
@ -168,11 +168,11 @@ export class UndoManager extends ObservableV2 {
deleteFilter = () => true, deleteFilter = () => true,
trackedOrigins = new Set([null]), trackedOrigins = new Set([null]),
ignoreRemoteMapChanges = false, 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() super()
/** /**
* @type {Array<AbstractType<any>>} * @type {Array<AbstractType<any> | Doc>}
*/ */
this.scope = [] this.scope = []
this.doc = doc this.doc = doc
@ -212,7 +212,7 @@ export class UndoManager extends ObservableV2 {
// Only track certain transactions // Only track certain transactions
if ( if (
!this.captureTransaction(transaction) || !this.captureTransaction(transaction) ||
!this.scope.some(type => transaction.changedParentTypes.has(type)) || !this.scope.some(type => transaction.changedParentTypes.has(/** @type {AbstractType<any>} */ (type)) || type === this.doc) ||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor))) (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
) { ) {
return return
@ -251,7 +251,7 @@ export class UndoManager extends ObservableV2 {
} }
// make sure that deleted structs are not gc'd // make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => { 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<any>} */ (type), item))) {
keepItem(item, true) keepItem(item, true)
} }
}) })
@ -272,13 +272,15 @@ export class UndoManager extends ObservableV2 {
} }
/** /**
* @param {Array<AbstractType<any>> | AbstractType<any>} ytypes * @param {Array<AbstractType<any> | Doc> | AbstractType<any> | Doc} ytypes
*/ */
addToScope (ytypes) { addToScope (ytypes) {
const tmpSet = new Set(this.scope)
ytypes = array.isArray(ytypes) ? ytypes : [ytypes] ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
ytypes.forEach(ytype => { ytypes.forEach(ytype => {
if (this.scope.every(yt => yt !== ytype)) { if (!tmpSet.has(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 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) this.scope.push(ytype)
} }
}) })

View File

@ -116,6 +116,19 @@ export const testEmptyTypeScope = _tc => {
t.assert(yarray.length === 0) 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 * Test case to fix #241
* @param {t.TestCase} _tc * @param {t.TestCase} _tc