make moved a separate prop on item
This commit is contained in:
parent
53a7b286b8
commit
56ab251e79
237
src/structs/ContentMove.js
Normal file
237
src/structs/ContentMove.js
Normal 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))
|
@ -282,14 +282,18 @@ export class Item extends AbstractStruct {
|
|||||||
*/
|
*/
|
||||||
this.parentSub = parentSub
|
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.
|
* 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 | null}
|
||||||
*
|
|
||||||
* @type {ID | Item | 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}
|
* @type {AbstractContent}
|
||||||
*/
|
*/
|
||||||
@ -299,57 +303,11 @@ export class Item extends AbstractStruct {
|
|||||||
* bit2: countable
|
* bit2: countable
|
||||||
* bit3: deleted
|
* bit3: deleted
|
||||||
* bit4: mark - mark node as fast-search-marker
|
* 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
|
* @type {number} byte
|
||||||
*/
|
*/
|
||||||
this.info = this.content.isCountable() ? binary.BIT2 : 0
|
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
|
* 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.client === right.id.client &&
|
||||||
this.id.clock + this.length === right.id.clock &&
|
this.id.clock + this.length === right.id.clock &&
|
||||||
this.deleted === right.deleted &&
|
this.deleted === right.deleted &&
|
||||||
this._ref === right._ref &&
|
this.redone === null &&
|
||||||
(!this.deleted || this.redone === null) &&
|
right.redone === null &&
|
||||||
|
this.moved === right.moved &&
|
||||||
this.content.constructor === right.content.constructor &&
|
this.content.constructor === right.content.constructor &&
|
||||||
this.content.mergeWith(right.content)
|
this.content.mergeWith(right.content)
|
||||||
) {
|
) {
|
||||||
|
@ -443,7 +443,7 @@ export class ListPosition {
|
|||||||
this.rel = 0
|
this.rel = 0
|
||||||
}
|
}
|
||||||
while (item && !this.reachedEnd && (len > 0 || (len === 0 && (!item.countable || item.deleted)))) {
|
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
|
len -= item.length
|
||||||
if (len <= 0) {
|
if (len <= 0) {
|
||||||
this.rel = -len
|
this.rel = -len
|
||||||
|
Loading…
x
Reference in New Issue
Block a user