diff --git a/src/structs/Item.js b/src/structs/Item.js index f1b82e37..656f5e5b 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -125,12 +125,13 @@ export const splitItem = (transaction, leftItem, diff) => { * @param {Transaction} transaction The Yjs instance. * @param {Item} item * @param {Set} redoitems + * @param {Array} itemsToDelete * * @return {Item|null} * * @private */ -export const redoItem = (transaction, item, redoitems) => { +export const redoItem = (transaction, item, redoitems, itemsToDelete) => { const doc = transaction.doc const store = doc.store const ownClientID = doc.clientID @@ -170,7 +171,7 @@ export const redoItem = (transaction, item, redoitems) => { // make sure that parent is redone if (parentItem !== null && parentItem.deleted === true && parentItem.redone === null) { // try to undo parent if it will be undone anyway - if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems) === null) { + if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null) { return null } } @@ -209,6 +210,11 @@ export const redoItem = (transaction, item, redoitems) => { } right = right.right } + // Iterate right while right is in itemsToDelete + // If it is intended to delete right while item is redone, we can expect that item should replace right. + while (left !== null && left.right !== null && left.right !== right && itemsToDelete.findIndex(d => d === /** @type {Item} */ (left).right) >= 0) { + left = left.right + } } const nextClock = getState(store, ownClientID) const nextId = createID(ownClientID, nextClock) diff --git a/src/types/YMap.js b/src/types/YMap.js index 6244f5a1..612bde0c 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -36,11 +36,11 @@ export class YMapEvent extends YEvent { } /** - * @template T number|string|Object|Array|Uint8Array + * @template MapType * A shared Map implementation. * - * @extends AbstractType> - * @implements {Iterable} + * @extends AbstractType> + * @implements {Iterable} */ export class YMap extends AbstractType { /** @@ -85,7 +85,7 @@ export class YMap extends AbstractType { } /** - * @return {YMap} + * @return {YMap} */ clone () { const map = new YMap() @@ -108,11 +108,11 @@ export class YMap extends AbstractType { /** * Transforms this Shared Type to a JSON object. * - * @return {Object} + * @return {Object} */ toJSON () { /** - * @type {Object} + * @type {Object} */ const map = {} this._map.forEach((item, key) => { @@ -163,11 +163,11 @@ export class YMap extends AbstractType { /** * Executes a provided function on once on every key-value pair. * - * @param {function(T,string,YMap):void} f A function to execute on every element of this YArray. + * @param {function(MapType,string,YMap):void} f A function to execute on every element of this YArray. */ forEach (f) { /** - * @type {Object} + * @type {Object} */ const map = {} this._map.forEach((item, key) => { @@ -179,7 +179,7 @@ export class YMap extends AbstractType { } /** - * @return {IterableIterator} + * @return {IterableIterator} */ [Symbol.iterator] () { return this.entries() @@ -204,7 +204,7 @@ export class YMap extends AbstractType { * Adds or updates an element with a specified key and value. * * @param {string} key The key of the element to add to this YMap - * @param {T} value The value of the element to add + * @param {MapType} value The value of the element to add */ set (key, value) { if (this.doc !== null) { @@ -221,7 +221,7 @@ export class YMap extends AbstractType { * Returns a specified element from this YMap. * * @param {string} key - * @return {T|undefined} + * @return {MapType|undefined} */ get (key) { return /** @type {any} */ (typeMapGet(this, key)) diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index b7bc80bf..391f82ac 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -88,7 +88,7 @@ const popStackItem = (undoManager, stack, eventType) => { } }) itemsToRedo.forEach(struct => { - performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange + performedChange = redoItem(transaction, struct, itemsToRedo, itemsToDelete) !== null || performedChange }) // We want to delete in reverse order so that children are deleted before // parents, so we have more information available when items are filtered. diff --git a/tests/undo-redo.tests.js b/tests/undo-redo.tests.js index f36e2ad6..813121b2 100644 --- a/tests/undo-redo.tests.js +++ b/tests/undo-redo.tests.js @@ -301,3 +301,58 @@ export const testUndoUntilChangePerformed = tc => { undoManager.undo() t.compareStrings(yMap2.get('key'), 'value') } + +/** + * This issue has been reported in https://github.com/yjs/yjs/issues/317 + * @param {t.TestCase} tc + */ +export const testUndoNestedUndoIssue = tc => { + const doc = new Y.Doc({ gc: false }) + const design = doc.getMap() + const undoManager = new Y.UndoManager(design, { captureTimeout: 0 }) + + /** + * @type {Y.Map} + */ + const text = new Y.Map() + + const blocks1 = new Y.Array() + const blocks1block = new Y.Map() + + doc.transact(() => { + blocks1block.set('text', 'Type Something') + blocks1.push([blocks1block]) + text.set('blocks', blocks1block) + design.set('text', text) + }) + + const blocks2 = new Y.Array() + const blocks2block = new Y.Map() + doc.transact(() => { + blocks2block.set('text', 'Something') + blocks2.push([blocks2block]) + text.set('blocks', blocks2block) + }) + + const blocks3 = new Y.Array() + const blocks3block = new Y.Map() + doc.transact(() => { + blocks3block.set('text', 'Something Else') + blocks3.push([blocks3block]) + text.set('blocks', blocks3block) + }) + + t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } }) + undoManager.undo() + t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } }) + undoManager.undo() + t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } }) + undoManager.undo() + t.compare(design.toJSON(), { }) + undoManager.redo() + t.compare(design.toJSON(), { text: { blocks: { text: 'Type Something' } } }) + undoManager.redo() + t.compare(design.toJSON(), { text: { blocks: { text: 'Something' } } }) + undoManager.redo() + t.compare(design.toJSON(), { text: { blocks: { text: 'Something Else' } } }) +}