diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 8fe3badb..21c3b694 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -9,6 +9,7 @@ import { createID, followRedone, getItemCleanStart, + getState, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line } from '../internals.js' @@ -49,23 +50,39 @@ const popStackItem = (undoManager, stack, eventType) => { transact(doc, transaction => { while (stack.length > 0 && result === null) { const store = doc.store + const clientID = doc.clientID const stackItem = /** @type {StackItem} */ (stack.pop()) + const stackStartClock = stackItem.start + const stackEndClock = stackItem.start + stackItem.len const itemsToRedo = new Set() + // @todo iterateStructs should not need the structs parameter + const structs = /** @type {Array} */ (store.clients.get(clientID)) let performedChange = false + if (stackStartClock !== stackEndClock) { + // make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end) + getItemCleanStart(transaction, createID(clientID, stackStartClock)) + if (stackEndClock < getState(doc.store, clientID)) { + getItemCleanStart(transaction, createID(clientID, stackEndClock)) + } + } iterateDeletedStructs(transaction, stackItem.ds, struct => { - if (struct instanceof Item && scope.some(type => isParentOf(type, struct))) { + if ( + struct instanceof Item && + scope.some(type => isParentOf(type, struct)) && + // Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval. + !(struct.id.client === clientID && struct.id.clock >= stackStartClock && struct.id.clock < stackEndClock) + ) { itemsToRedo.add(struct) } }) itemsToRedo.forEach(item => { performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange }) - const structs = /** @type {Array} */ (store.clients.get(doc.clientID)) /** * @type {Array} */ const itemsToDelete = [] - iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => { + iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => { if (struct instanceof Item && !struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { if (struct.redone !== null) { let { item, diff } = followRedone(store, struct.id) @@ -73,7 +90,7 @@ const popStackItem = (undoManager, stack, eventType) => { item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff)) } if (item.length > stackItem.len) { - getItemCleanStart(transaction, createID(item.id.client, item.id.clock + stackItem.len)) + getItemCleanStart(transaction, createID(item.id.client, stackEndClock)) } struct = item } diff --git a/tests/undo-redo.tests.js b/tests/undo-redo.tests.js index cc4ee027..cb85233e 100644 --- a/tests/undo-redo.tests.js +++ b/tests/undo-redo.tests.js @@ -13,6 +13,10 @@ import * as t from 'lib0/testing.js' export const testUndoText = tc => { const { testConnector, text0, text1 } = init(tc, { users: 3 }) const undoManager = new UndoManager(text0) + text0.insert(0, 'test') + text0.delete(0, 4) + undoManager.undo() + t.assert(text0.toString() === '') text0.insert(0, 'abc') text1.insert(0, 'xyz') testConnector.syncAll() @@ -65,6 +69,15 @@ export const testUndoMap = tc => { t.assert(map0.get('a') === 44) undoManager.redo() t.assert(map0.get('a') === 44) + + // test setting value multiple times + map0.set('b', 'initial') + undoManager.stopCapturing() + map0.set('b', 'val1') + map0.set('b', 'val2') + undoManager.stopCapturing() + undoManager.undo() + t.assert(map0.get('b') === 'initial') } /**