Rework UndoManager to support changes from other / multiple users

This commit is contained in:
Kevin Jahns 2020-05-13 19:28:30 +02:00
parent a70c5112cd
commit dab172fa1d

View File

@ -19,13 +19,13 @@ import { Observable } from 'lib0/observable.js'
class StackItem { class StackItem {
/** /**
* @param {DeleteSet} ds * @param {DeleteSet} ds
* @param {number} start clock start of the local client * @param {Map<number,number>} beforeState
* @param {number} len * @param {Map<number,number>} afterState
*/ */
constructor (ds, start, len) { constructor (ds, beforeState, afterState) {
this.ds = ds this.ds = ds
this.start = start this.beforeState = beforeState
this.len = len this.afterState = afterState
/** /**
* Use this to save and restore metadata like selection range * Use this to save and restore metadata like selection range
*/ */
@ -50,47 +50,37 @@ const popStackItem = (undoManager, stack, eventType) => {
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && result === null) {
const store = doc.store const store = doc.store
const clientID = doc.clientID
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
const stackStartClock = stackItem.start /**
const stackEndClock = stackItem.start + stackItem.len * @type {Set<Item>}
*/
const itemsToRedo = new Set() const itemsToRedo = new Set()
// @todo iterateStructs should not need the structs parameter
const structs = /** @type {Array<GC|Item>} */ (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)) &&
// 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(struct => {
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
})
/** /**
* @type {Array<Item>} * @type {Array<Item>}
*/ */
const itemsToDelete = [] const itemsToDelete = []
iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => { let performedChange = false
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<GC|Item>} */ (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 instanceof Item) {
if (struct.redone !== null) { if (struct.redone !== null) {
let { item, diff } = followRedone(store, struct.id) let { item, diff } = followRedone(store, struct.id)
if (diff > 0) { if (diff > 0) {
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff)) item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
} }
if (item.length > stackItem.len) { if (item.length > len) {
getItemCleanStart(transaction, createID(item.id.client, stackEndClock)) getItemCleanStart(transaction, createID(item.id.client, endClock))
} }
struct = item struct = item
} }
@ -99,6 +89,26 @@ const popStackItem = (undoManager, stack, eventType) => {
} }
} }
}) })
}
})
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.
!(clock >= startClock && clock < endClock)
) {
itemsToRedo.add(struct)
}
})
itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
})
// We want to delete in reverse order so that children are deleted before // We want to delete in reverse order so that children are deleted before
// parents, so we have more information available when items are filtered. // parents, so we have more information available when items are filtered.
for (let i = itemsToDelete.length - 1; i >= 0; i--) { for (let i = itemsToDelete.length - 1; i >= 0; i--) {
@ -181,17 +191,17 @@ export class UndoManager extends Observable {
// neither undoing nor redoing: delete redoStack // neither undoing nor redoing: delete redoStack
this.redoStack = [] this.redoStack = []
} }
const beforeState = transaction.beforeState.get(this.doc.clientID) || 0 const beforeState = transaction.beforeState
const afterState = transaction.afterState.get(this.doc.clientID) || 0 const afterState = transaction.afterState
const now = time.getUnixTime() const now = time.getUnixTime()
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) { if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op // append change to last stack op
const lastOp = stack[stack.length - 1] const lastOp = stack[stack.length - 1]
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet]) lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
lastOp.len = afterState - lastOp.start lastOp.afterState = afterState
} else { } else {
// create a new stack op // 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) { if (!undoing && !redoing) {
this.lastChange = now this.lastChange = now