fix all remaining bugs for single-item moves (mainly event bugs)

This commit is contained in:
Kevin Jahns 2022-04-04 13:10:43 +02:00
parent 51c095ec52
commit b32f88cd40
6 changed files with 118 additions and 104 deletions

View File

@ -11,7 +11,7 @@ import { decodeRelativePosition, encodeRelativePosition } from 'yjs'
/**
* @param {ContentMove} moved
* @param {Transaction} tr
* @return {{ start: Item, end: Item | null }} $start (inclusive) is the beginning and $end (exclusive) is the end of the moved area
* @return {{ start: Item, end: Item }} $start (inclusive) is the beginning and $end (inclusive) is the end of the moved area
*/
export const getMovedCoords = (moved, tr) => {
let start // this (inclusive) is the beginning of the moved area
@ -38,36 +38,9 @@ export const getMovedCoords = (moved, tr) => {
end = getItemCleanStart(tr, moved.end.item)
}
} else {
end = null
}
return { start: /** @type {Item} */ (start), end }
}
/**
* @todo remove this if not needed
*
* @param {ContentMove} moved
* @param {Item} movedItem
* @param {Transaction} tr
* @param {function(Item):void} cb
*/
export const iterateMoved = (moved, movedItem, tr, cb) => {
/**
* @type {{ start: Item | null, end: Item | null }}
*/
let { start, end } = getMovedCoords(moved, tr)
while (start !== end && start != null) {
if (!start.deleted) {
if (start.moved === movedItem) {
if (start.content.constructor === ContentMove) {
iterateMoved(start.content, start, tr, cb)
} else {
cb(start)
}
}
}
start = start.right
error.unexpectedCase()
}
return { start: /** @type {Item} */ (start), end: /** @type {Item} */ (end) }
}
/**
@ -172,7 +145,8 @@ export class ContentMove {
* @param {Item} item
*/
integrate (transaction, item) {
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = []
const sm = /** @type {AbstractType<any>} */ (item.parent)._searchMarker
if (sm) sm.length = 0
/**
* @type {{ start: Item | null, end: Item | null }}
*/
@ -182,25 +156,23 @@ export class ContentMove {
// that we want to set prio to the current prio-maximum of the moved range.
const adaptPriority = this.priority < 0
while (start !== end && start != null) {
if (!start.deleted) {
const currMoved = start.moved
const nextPrio = currMoved ? /** @type {ContentMove} */ (currMoved.content).priority : -1
if (adaptPriority || nextPrio < this.priority || (currMoved != null && nextPrio === this.priority && (currMoved.id.client < item.id.client || (currMoved.id.client === item.id.client && currMoved.id.clock < item.id.clock)))) {
if (currMoved !== null) {
this.overrides.add(currMoved)
}
maxPriority = math.max(maxPriority, nextPrio)
// was already moved
const prevMove = start.moved
if (prevMove && !transaction.prevMoved.has(start) && prevMove.id.clock < (transaction.beforeState.get(prevMove.id.client) || 0)) {
// only override prevMoved if the prevMoved item is not new
// we need to know which item previously moved an item
transaction.prevMoved.set(start, prevMove)
}
start.moved = item
} else if (currMoved != null) {
/** @type {ContentMove} */ (currMoved.content).overrides.add(item)
const currMoved = start.moved
const nextPrio = currMoved ? /** @type {ContentMove} */ (currMoved.content).priority : -1
if (adaptPriority || nextPrio < this.priority || (currMoved != null && nextPrio === this.priority && (currMoved.id.client < item.id.client || (currMoved.id.client === item.id.client && currMoved.id.clock < item.id.clock)))) {
if (currMoved !== null) {
this.overrides.add(currMoved)
}
maxPriority = math.max(maxPriority, nextPrio)
// was already moved
const prevMove = start.moved
if (prevMove && !transaction.prevMoved.has(start) && prevMove.id.clock < (transaction.beforeState.get(prevMove.id.client) || 0)) {
// only override prevMoved if the prevMoved item is not new
// we need to know which item previously moved an item
transaction.prevMoved.set(start, prevMove)
}
start.moved = item
} else if (currMoved != null) {
/** @type {ContentMove} */ (currMoved.content).overrides.add(item)
}
start = start.right
}

View File

@ -652,7 +652,6 @@ export class Item extends AbstractStruct {
if (!this.deleted) {
throw error.unexpectedCase()
}
this.moved = null
this.content.gc(store)
if (parentGCd) {
replaceStruct(store, this, new GC(this.id, this.length))

View File

@ -36,7 +36,7 @@ const maxSearchMarker = 80
*/
export const useSearchMarker = (tr, yarray, index, f) => {
const searchMarker = yarray._searchMarker
if (searchMarker === null || yarray._start === null || index < 5) {
if (searchMarker === null || yarray._start === null) { // @todo add condition `index < 5`
return f(new ListIterator(yarray).forward(tr, index))
}
if (searchMarker.length === 0) {
@ -47,7 +47,7 @@ export const useSearchMarker = (tr, yarray, index, f) => {
const sm = searchMarker.reduce(
(a, b, arrayIndex) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b
)
const newIsCheaper = math.abs(sm.index - index) > index
const newIsCheaper = math.abs(sm.index - index) > index // @todo use >= index
const createFreshMarker = searchMarker.length < maxSearchMarker && (math.abs(sm.index - index) > 5 || newIsCheaper)
const fsm = createFreshMarker ? (newIsCheaper ? new ListIterator(yarray) : sm.clone()) : sm
const prevItem = /** @type {Item} */ (sm.nextItem)
@ -60,7 +60,7 @@ export const useSearchMarker = (tr, yarray, index, f) => {
} else {
fsm.forward(tr, -diff)
}
// @todo remove this tests
// @todo remove this test
/*
const otherTesting = new ListIterator(yarray)
otherTesting.forward(tr, index)
@ -77,10 +77,8 @@ export const useSearchMarker = (tr, yarray, index, f) => {
}
fsm.rel = 0
}
if (fsm.rel > 0) {
fsm.index -= fsm.rel
fsm.rel = 0
}
fsm.index -= fsm.rel
fsm.rel = 0
if (!createFreshMarker) {
// reused old marker and we moved to a different position
prevItem.marker = false

View File

@ -11,11 +11,55 @@ import {
ContentType,
ContentDoc,
Doc,
compareIDs,
RelativePosition, ID, AbstractContent, ContentMove, Transaction, Item, AbstractType // eslint-disable-line
} from '../internals.js'
const lengthExceeded = error.create('Length exceeded!')
/**
* We keep the moved-stack across several transactions. Local or remote changes can invalidate
* "moved coords" on the moved-stack.
*
* The reason for this is that if assoc < 0, then getMovedCoords will return the target.right item.
* While the computed item is on the stack, it is possible that a user inserts something between target
* and the item on the stack. Then we expect that the newly inserted item is supposed to be on the new
* computed item.
*
* @param {Transaction} tr
* @param {ListIterator} li
*/
const popMovedStack = (tr, li) => {
let { start, end, move } = li.movedStack.pop() || { start: null, end: null, move: null }
if (move) {
const moveContent = /** @type {ContentMove} */ (move.content)
if (
(
moveContent.start.assoc < 0 && (
(start === null && moveContent.start.item !== null) ||
(start !== null && !compareIDs(/** @type {Item} */ (start.left).lastId, moveContent.start.item))
)
) || (
moveContent.end.assoc < 0 && (
(end === null && moveContent.end.item !== null) ||
(end !== null && !compareIDs(/** @type {Item} */ (end.left).lastId, moveContent.end.item))
)
)
) {
const coords = getMovedCoords(moveContent, tr)
start = coords.start
end = coords.end
// @todo why are we not running into this?
console.log('found edge case 42') // @todo remove
debugger
}
}
li.currMove = move
li.currMoveStart = start
li.currMoveEnd = end
li.reachedEnd = false
}
/**
* @todo rename to walker?
* @todo check that inserting character one after another always reuses ListIterators
@ -113,10 +157,13 @@ export class ListIterator {
* @param {number} len
*/
forward (tr, len) {
if (this.index + len > this.type._length) {
if (len === 0 && this.nextItem == null) {
return this
}
if (this.index + len > this.type._length || this.nextItem == null) {
throw lengthExceeded
}
let item = this.nextItem
let item = /** @type {Item} */ (this.nextItem)
this.index += len
// @todo this condition is not needed, better to remove it (can always be applied)
if (this.rel) {
@ -126,13 +173,9 @@ export class ListIterator {
while ((!this.reachedEnd || this.currMove !== null) && (len > 0 || (len === 0 && item && (!item.countable || item.deleted || item === this.currMoveEnd || (this.reachedEnd && this.currMoveEnd === null) || item.moved !== this.currMove)))) {
if (item === this.currMoveEnd || (this.currMoveEnd === null && this.reachedEnd && this.currMove)) {
item = /** @type {Item} */ (this.currMove) // we iterate to the right after the current condition
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
this.currMove = move
this.currMoveStart = start
this.currMoveEnd = end
this.reachedEnd = false
popMovedStack(tr, this)
} else if (item === null) {
break
error.unexpectedCase() // should never happen
} else if (item.countable && !item.deleted && item.moved === this.currMove && len > 0) {
len -= item.length
if (len < 0) {
@ -151,10 +194,13 @@ export class ListIterator {
item = start
continue
}
if (this.reachedEnd) {
throw error.unexpectedCase
}
if (item.right) {
item = item.right
} else {
this.reachedEnd = true
this.reachedEnd = true // @todo we need to ensure to iterate further if this.currMoveEnd === null
}
}
this.index -= len
@ -170,10 +216,7 @@ export class ListIterator {
if (item !== null) {
while (item === this.currMoveStart) {
item = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
this.currMove = move
this.currMoveStart = start
this.currMoveEnd = end
popMovedStack(tr, this)
}
this.nextItem = item
}
@ -228,10 +271,7 @@ export class ListIterator {
}
if (item === this.currMoveStart) {
item = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
const { start, end, move } = this.movedStack.pop() || { start: null, end: null, move: null }
this.currMove = move
this.currMoveStart = start
this.currMoveEnd = end
popMovedStack(tr, this)
}
item = item.left
}
@ -248,33 +288,45 @@ export class ListIterator {
* @param {function(T, T): T} concat
*/
_slice (tr, len, value, slice, concat) {
if (this.index + len > this.type._length) {
throw lengthExceeded
}
this.index += len
/**
* We store nextItem in a variable because this version cannot be null.
*/
let nextItem = /** @type {Item} */ (this.nextItem)
while (len > 0 && !this.reachedEnd) {
while (this.nextItem && this.nextItem.countable && !this.reachedEnd && len > 0 && this.nextItem !== this.currMoveEnd) {
if (!this.nextItem.deleted && this.nextItem.moved === this.currMove) {
const item = this.nextItem
const slicedContent = slice(item.content, this.rel, len)
while (nextItem.countable && !this.reachedEnd && len > 0 && nextItem !== this.currMoveEnd) {
if (!nextItem.deleted && nextItem.moved === this.currMove) {
const slicedContent = slice(nextItem.content, this.rel, len)
len -= slicedContent.length
value = concat(value, slicedContent)
if (item.length !== slicedContent.length) {
if (this.rel + slicedContent.length === item.length) {
this.rel = 0
} else {
this.rel += slicedContent.length
continue // do not iterate to item.right
}
if (this.rel + slicedContent.length === nextItem.length) {
this.rel = 0
} else {
this.rel += slicedContent.length
continue // do not iterate to item.right
}
}
if (this.nextItem.right) {
this.nextItem = this.nextItem.right
if (nextItem.right) {
nextItem = nextItem.right
this.nextItem = nextItem
} else {
this.reachedEnd = true
}
}
if (this.nextItem && (!this.reachedEnd || this.currMove !== null) && len > 0) {
if ((!this.reachedEnd || this.currMove !== null) && len > 0) {
// always set nextItem before any method call
this.nextItem = nextItem
this.forward(tr, 0)
if (this.nextItem == null) {
throw new Error('debug me') // @todo remove
}
nextItem = this.nextItem
}
}
this.nextItem = nextItem
if (len < 0) {
this.index -= len
}
@ -378,7 +430,7 @@ export class ListIterator {
// @todo is there a better alrogirthm to update searchmarkers? We could simply remove the markers that are in the updated range.
// Also note that searchmarkers are updated in insertContents as well.
const sm = this.type._searchMarker
if (sm) sm.length = 0
if (sm) sm.length = 0 // @todo instead, iterate through sm and delete all marked properties on items
}
/**

View File

@ -215,7 +215,7 @@ export class YEvent {
continue // do not move to item.right
}
} else if (item.moved !== currMove) {
if (!currMoveIsNew && item.countable && item.moved && !this.adds(item) && this.adds(item.moved) && (this.transaction.prevMoved.get(item) || null) === currMove) {
if (!currMoveIsNew && item.countable && (!item.deleted || this.deletes(item)) && item.moved && !this.adds(item) && this.adds(item.moved) && (this.transaction.prevMoved.get(item) || null) === currMove) {
if (lastOp === null || lastOp.delete === undefined) {
packOp()
lastOp = { delete: 0 }

View File

@ -540,20 +540,6 @@ const getUniqueNumber = () => _uniqueNumber++
* @todo to replace content to a separate data structure so we know that insert & returns work as expected!!!
*/
const arrayTransactions = [
function move (user, gen) {
const yarray = user.getArray('array')
if (yarray.length === 0) {
return
}
const pos = prng.int32(gen, 0, yarray.length - 1)
const newPos = prng.int32(gen, 0, yarray.length)
const oldContent = yarray.toArray()
yarray.move(pos, newPos)
const [x] = oldContent.splice(pos, 1)
oldContent.splice(pos < newPos ? newPos - 1 : newPos, 0, x)
t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position
},
// @todo remove this duplicate!!
function move (user, gen) {
const yarray = user.getArray('array')
if (yarray.length === 0) {
@ -671,6 +657,13 @@ export const testRepeatGeneratingYarrayTests6 = tc => {
compareTestobjects(applyRandomTests(tc, arrayTransactions, 6, monitorArrayTestObject))
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatGeneratingYarrayTests10 = tc => {
compareTestobjects(applyRandomTests(tc, arrayTransactions, 10, monitorArrayTestObject))
}
/**
* @param {t.TestCase} tc
*/