From dab172fa1d1c906c9d9ba4577dae9c58e8c80c0c Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Wed, 13 May 2020 19:28:30 +0200 Subject: [PATCH] Rework UndoManager to support changes from other / multiple users --- src/utils/UndoManager.js | 94 ++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 286aa84d..4c9717b9 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -19,13 +19,13 @@ import { Observable } from 'lib0/observable.js' class StackItem { /** * @param {DeleteSet} ds - * @param {number} start clock start of the local client - * @param {number} len + * @param {Map} beforeState + * @param {Map} afterState */ - constructor (ds, start, len) { + constructor (ds, beforeState, afterState) { this.ds = ds - this.start = start - this.len = len + this.beforeState = beforeState + this.afterState = afterState /** * Use this to save and restore metadata like selection range */ @@ -50,27 +50,58 @@ 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 + /** + * @type {Set} + */ const itemsToRedo = new Set() - // @todo iterateStructs should not need the structs parameter - const structs = /** @type {Array} */ (store.clients.get(clientID)) + /** + * @type {Array} + */ + const itemsToDelete = [] 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)) + stackItem.afterState.forEach((endClock, client) => { + const startClock = stackItem.beforeState.get(client) || 0 + const len = endClock - startClock + // @todo iterateStructs should not need the structs parameter + const structs = /** @type {Array} */ (store.clients.get(client)) + if (startClock !== endClock) { + // make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end) + // this must be executed before deleted structs are iterated. + getItemCleanStart(transaction, createID(client, startClock)) + if (endClock < getState(doc.store, client)) { + getItemCleanStart(transaction, createID(client, endClock)) + } + iterateStructs(transaction, structs, startClock, len, struct => { + if (struct instanceof Item) { + if (struct.redone !== null) { + let { item, diff } = followRedone(store, struct.id) + if (diff > 0) { + item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff)) + } + if (item.length > len) { + getItemCleanStart(transaction, createID(item.id.client, endClock)) + } + struct = item + } + if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { + itemsToDelete.push(struct) + } + } + }) } - } + }) iterateDeletedStructs(transaction, stackItem.ds, struct => { + const id = struct.id + const clock = id.clock + const client = id.client + const startClock = stackItem.beforeState.get(client) || 0 + const endClock = stackItem.afterState.get(client) || 0 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) + !(clock >= startClock && clock < endClock) ) { itemsToRedo.add(struct) } @@ -78,27 +109,6 @@ const popStackItem = (undoManager, stack, eventType) => { itemsToRedo.forEach(struct => { performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange }) - /** - * @type {Array} - */ - const itemsToDelete = [] - iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => { - if (struct instanceof Item) { - if (struct.redone !== null) { - let { item, diff } = followRedone(store, struct.id) - if (diff > 0) { - item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff)) - } - if (item.length > stackItem.len) { - getItemCleanStart(transaction, createID(item.id.client, stackEndClock)) - } - struct = item - } - if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { - itemsToDelete.push(struct) - } - } - }) // We want to delete in reverse order so that children are deleted before // parents, so we have more information available when items are filtered. for (let i = itemsToDelete.length - 1; i >= 0; i--) { @@ -181,17 +191,17 @@ export class UndoManager extends Observable { // neither undoing nor redoing: delete redoStack this.redoStack = [] } - const beforeState = transaction.beforeState.get(this.doc.clientID) || 0 - const afterState = transaction.afterState.get(this.doc.clientID) || 0 + const beforeState = transaction.beforeState + const afterState = transaction.afterState const now = time.getUnixTime() if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) { // append change to last stack op const lastOp = stack[stack.length - 1] lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet]) - lastOp.len = afterState - lastOp.start + lastOp.afterState = afterState } else { // create a new stack op - stack.push(new StackItem(transaction.deleteSet, beforeState, afterState - beforeState)) + stack.push(new StackItem(transaction.deleteSet, beforeState, afterState)) } if (!undoing && !redoing) { this.lastChange = now