Rework UndoManager to support changes from other / multiple users
This commit is contained in:
parent
a70c5112cd
commit
dab172fa1d
@ -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,27 +50,58 @@ 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))
|
* @type {Array<Item>}
|
||||||
|
*/
|
||||||
|
const itemsToDelete = []
|
||||||
let performedChange = false
|
let performedChange = false
|
||||||
if (stackStartClock !== stackEndClock) {
|
stackItem.afterState.forEach((endClock, client) => {
|
||||||
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
|
const startClock = stackItem.beforeState.get(client) || 0
|
||||||
getItemCleanStart(transaction, createID(clientID, stackStartClock))
|
const len = endClock - startClock
|
||||||
if (stackEndClock < getState(doc.store, clientID)) {
|
// @todo iterateStructs should not need the structs parameter
|
||||||
getItemCleanStart(transaction, createID(clientID, stackEndClock))
|
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.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 => {
|
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 (
|
if (
|
||||||
struct instanceof Item &&
|
struct instanceof Item &&
|
||||||
scope.some(type => isParentOf(type, struct)) &&
|
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.
|
// 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)
|
itemsToRedo.add(struct)
|
||||||
}
|
}
|
||||||
@ -78,27 +109,6 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
itemsToRedo.forEach(struct => {
|
itemsToRedo.forEach(struct => {
|
||||||
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
|
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
|
||||||
})
|
})
|
||||||
/**
|
|
||||||
* @type {Array<Item>}
|
|
||||||
*/
|
|
||||||
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
|
// 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user