make moved a separate prop on item

This commit is contained in:
Kevin Jahns 2021-11-08 18:33:26 +01:00
parent 53a7b286b8
commit 56ab251e79
3 changed files with 250 additions and 54 deletions

237
src/structs/ContentMove.js Normal file
View File

@ -0,0 +1,237 @@
import * as error from 'lib0/error'
import * as decoding from 'lib0/decoding'
import {
AbstractType, ContentType, ID, RelativePosition, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore, getItem, getItemCleanStart, getItemCleanEnd // eslint-disable-line
} from '../internals.js'
/**
* @param {ContentMove} moved
* @param {Transaction} tr
* @return {{ start: Item | null, end: Item | null }} $start (inclusive) is the beginning and $end (exclusive) is the end of the moved area
*/
export const getMovedCoords = (moved, tr) => {
let start // this (inclusive) is the beginning of the moved area
let end // this (exclusive) is the first item after start that is not part of the moved area
if (moved.start.item) {
if (moved.start.assoc < 0) {
start = getItemCleanEnd(tr, moved.start.item)
start = start.right
} else {
start = getItemCleanStart(tr, moved.start.item)
}
} else if (moved.start.tname != null) {
start = tr.doc.get(moved.start.tname)._start
} else if (moved.start.type) {
start = /** @type {ContentType} */ (getItem(tr.doc.store, moved.start.type).content).type._start
} else {
error.unexpectedCase()
}
if (moved.end.item) {
if (moved.end.assoc < 0) {
end = getItemCleanEnd(tr, moved.end.item)
end = end.right
} else {
end = getItemCleanStart(tr, moved.end.item)
}
} else {
end = null
}
return { start, end }
}
/**
* @param {ContentMove} moved
* @param {Item} movedItem
* @param {Transaction} tr
* @param {function(Item):void} cb
*/
export const iterateMoved = (moved, movedItem, tr, cb) => {
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
}
}
/**
* @param {ContentMove} moved
* @param {Item} movedItem
* @param {Set<Item>} trackedMovedItems
* @param {Transaction} tr
* @return {boolean} true if there is a loop
*/
export const findMoveLoop = (moved, movedItem, trackedMovedItems, tr) => {
if (trackedMovedItems.has(movedItem)) {
return true
}
trackedMovedItems.add(movedItem)
let { start, end } = getMovedCoords(moved, tr)
while (start !== end && start != null) {
if (start.deleted && start.moved === movedItem && start.content.constructor === ContentMove) {
if (findMoveLoop(start.content, start, trackedMovedItems, tr)) {
return true
}
}
start = start.right
}
return false
}
/**
* @private
*/
export class ContentMove {
/**
* @param {RelativePosition} start
* @param {RelativePosition} end
* @param {number} priority if we want to move content that is already moved, we need to assign a higher priority to this move operation.
*/
constructor (start, end, priority) {
this.start = start
this.end = end
this.priority = priority
/**
* We store which Items+ContentMove we override. Once we delete
* this ContentMove, we need to re-integrate the overridden items.
*
* This representation can be improved if we ever run into memory issues because of too many overrides.
* Ideally, we should probably just re-iterate the document and re-integrate all moved items.
* This is fast enough and reduces memory footprint significantly.
*
* @type {Set<Item>}
*/
this.overrides = new Set()
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [null]
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentMove}
*/
copy () {
return new ContentMove(this.start, this.end, this.priority)
}
/**
* @param {number} offset
* @return {ContentMove}
*/
splice (offset) {
return this
}
/**
* @param {ContentMove} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = []
let { start, end } = getMovedCoords(this, transaction)
while (start !== end && start != null) {
if (!start.deleted) {
const currMoved = start.moved
if (currMoved === null || /** @type {ContentMove} */ (currMoved.content).priority < 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)
}
start.moved = item
} else {
/** @type {ContentMove} */ (currMoved.content).overrides.add(item)
}
}
start = start.right
}
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
delete (transaction, item) {
let { start, end } = getMovedCoords(this, transaction)
while (start !== end && start != null) {
if (start.moved === item) {
start.moved = null
}
start = start.right
}
/**
* @param {Item} reIntegrateItem
*/
const reIntegrate = reIntegrateItem => {
const content = /** @type {ContentMove} */ (reIntegrateItem.content)
if (reIntegrateItem.deleted) {
// potentially we can integrate the items that reIntegrateItem overrides
content.overrides.forEach(reIntegrate)
} else {
content.integrate(transaction, reIntegrateItem)
}
}
this.overrides.forEach(reIntegrate)
}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeAny(this.start)
encoder.writeAny(this.end)
}
/**
* @return {number}
*/
getRef () {
return 11
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentMove}
*/
export const readContentMove = decoder => new ContentMove(decoder.readAny(), decoder.readAny(), decoding.readVarUint(decoder.restDecoder))

View File

@ -282,14 +282,18 @@ export class Item extends AbstractStruct {
*/
this.parentSub = parentSub
/**
* If this type is deleted: If this type's effect is reundone this type refers to the type-id that undid
* If this type's effect is reundone this type refers to the type-id that undid
* this operation.
*
* If this item is not deleted: This property is reused by the moved prop. In this case this property refers to an Item.
*
* @type {ID | Item | null}
* @type {ID | null}
*/
this._ref = null
this.redone = null
/**
* This property is reused by the moved prop. In this case this property refers to an Item.
*
* @type {Item | null}
*/
this.moved = null
/**
* @type {AbstractContent}
*/
@ -299,57 +303,11 @@ export class Item extends AbstractStruct {
* bit2: countable
* bit3: deleted
* bit4: mark - mark node as fast-search-marker
* bit5: moved - whether this item has been moved. The moved item is then referred to on the "redone" prop.
* @type {number} byte
*/
this.info = this.content.isCountable() ? binary.BIT2 : 0
}
/**
* If this type's effect is reundone this type refers to the type-id that undid
* this operation.
*
* @return {ID | null}
*/
get redone () {
return /** @type {ID | null} */ (this._ref)
}
/**
* @param {ID | null} id
*/
set redone (id) {
this._ref = id
}
/**
* If this item has been moved, the moved property will referr to the item that moved this content.
*
* @param {Item | null} item
*/
set movedBy (item) {
this._ref = item
if (item != null) {
this.info |= binary.BIT5
} else if (this.moved) {
this.info ^= binary.BIT5
}
}
/**
* @return {Item | null}
*/
get movedBy () {
return this.moved ? /** @type {Item} */ (this._ref) : null
}
/**
* @return {boolean}
*/
get moved () {
return (this.info & binary.BIT5) > 0
}
/**
* This is used to mark the item as an indexed fast-search marker
*
@ -617,8 +575,9 @@ export class Item extends AbstractStruct {
this.id.client === right.id.client &&
this.id.clock + this.length === right.id.clock &&
this.deleted === right.deleted &&
this._ref === right._ref &&
(!this.deleted || this.redone === null) &&
this.redone === null &&
right.redone === null &&
this.moved === right.moved &&
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {

View File

@ -443,7 +443,7 @@ export class ListPosition {
this.rel = 0
}
while (item && !this.reachedEnd && (len > 0 || (len === 0 && (!item.countable || item.deleted)))) {
if (item.countable && !item.deleted && item.movedBy === this.currMove) {
if (item.countable && !item.deleted && item.moved === this.currMove) {
len -= item.length
if (len <= 0) {
this.rel = -len