Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b56debef00 | ||
|
|
5b16071380 | ||
|
|
7ced59c847 | ||
|
|
3c98d97369 | ||
|
|
56d747faea | ||
|
|
a3b97d941b | ||
|
|
efcfe4b483 | ||
|
|
4de3c004a8 | ||
|
|
100e436e2c | ||
|
|
3b31764b6e | ||
|
|
19723670c4 | ||
|
|
0ce40596d1 | ||
|
|
4078e115c1 | ||
|
|
ab5061cd47 | ||
|
|
44499cb9fe | ||
|
|
b63d22e7db | ||
|
|
bf05061cc7 | ||
|
|
7e9319f82e | ||
|
|
2e9a7df603 | ||
|
|
1f99e8203a | ||
|
|
69b7f4bfb9 | ||
|
|
b2b7b8c280 | ||
|
|
a0c9235a36 | ||
|
|
e8ecc8f74b | ||
|
|
b32f88cd40 | ||
|
|
51c095ec52 | ||
|
|
285dc79a6b | ||
|
|
f65d1b8475 | ||
|
|
c4b28aceec | ||
|
|
cc93f346ce | ||
|
|
d3dcd24ef4 |
@@ -482,8 +482,6 @@ or any of its children.
|
|||||||
<dd>Clone this type into a fresh Yjs type.</dd>
|
<dd>Clone this type into a fresh Yjs type.</dd>
|
||||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||||
<dd>Copies the children to a new Array.</dd>
|
<dd>Copies the children to a new Array.</dd>
|
||||||
<b><code>toDOM():DocumentFragment</code></b>
|
|
||||||
<dd>Transforms this type and all children to new DOM elements.</dd>
|
|
||||||
<b><code>toString():string</code></b>
|
<b><code>toString():string</code></b>
|
||||||
<dd>Get the XML serialization of all descendants.</dd>
|
<dd>Get the XML serialization of all descendants.</dd>
|
||||||
<b><code>toJSON():string</code></b>
|
<b><code>toJSON():string</code></b>
|
||||||
@@ -557,8 +555,6 @@ content and be actually XML compliant.
|
|||||||
<dd>Clone this type into a fresh Yjs type.</dd>
|
<dd>Clone this type into a fresh Yjs type.</dd>
|
||||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||||
<dd>Copies the children to a new Array.</dd>
|
<dd>Copies the children to a new Array.</dd>
|
||||||
<b><code>toDOM():Element</code></b>
|
|
||||||
<dd>Transforms this type and all children to a new DOM element.</dd>
|
|
||||||
<b><code>toString():string</code></b>
|
<b><code>toString():string</code></b>
|
||||||
<dd>Get the XML serialization of all descendants.</dd>
|
<dd>Get the XML serialization of all descendants.</dd>
|
||||||
<b><code>toJSON():string</code></b>
|
<b><code>toJSON():string</code></b>
|
||||||
|
|||||||
3958
package-lock.json
generated
3958
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.6.0-1",
|
"version": "14.0.0-1",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export {
|
|||||||
findRootTypeKey,
|
findRootTypeKey,
|
||||||
findIndexSS,
|
findIndexSS,
|
||||||
getItem,
|
getItem,
|
||||||
typeListToArraySnapshot,
|
|
||||||
typeMapGetSnapshot,
|
typeMapGetSnapshot,
|
||||||
createDocFromSnapshot,
|
createDocFromSnapshot,
|
||||||
iterateDeletedStructs,
|
iterateDeletedStructs,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export * from './utils/encoding.js'
|
|||||||
export * from './utils/EventHandler.js'
|
export * from './utils/EventHandler.js'
|
||||||
export * from './utils/ID.js'
|
export * from './utils/ID.js'
|
||||||
export * from './utils/isParentOf.js'
|
export * from './utils/isParentOf.js'
|
||||||
export * from './utils/ListIterator.js'
|
export * from './utils/ListCursor.js'
|
||||||
export * from './utils/logging.js'
|
export * from './utils/logging.js'
|
||||||
export * from './utils/PermanentUserData.js'
|
export * from './utils/PermanentUserData.js'
|
||||||
export * from './utils/RelativePosition.js'
|
export * from './utils/RelativePosition.js'
|
||||||
|
|||||||
@@ -4,80 +4,60 @@ import * as decoding from 'lib0/decoding'
|
|||||||
import * as encoding from 'lib0/encoding'
|
import * as encoding from 'lib0/encoding'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
import {
|
import {
|
||||||
AbstractType, ContentType, RelativePosition, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore, getItem, getItemCleanStart, getItemCleanEnd // eslint-disable-line
|
writeID,
|
||||||
|
readID,
|
||||||
|
ID, AbstractType, ContentType, RelativePosition, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore, getItem, getItemCleanStart, getItemCleanEnd, // eslint-disable-line
|
||||||
|
addsStruct
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
import { decodeRelativePosition, encodeRelativePosition } from 'yjs'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ContentMove} moved
|
* @param {ContentMove | { start: RelativePosition, end: RelativePosition }} moved
|
||||||
* @param {Transaction} tr
|
* @param {Transaction} tr
|
||||||
* @return {{ start: Item, end: Item | null }} $start (inclusive) is the beginning and $end (exclusive) is the end of the moved area
|
* @param {boolean} split
|
||||||
|
* @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) => {
|
export const getMovedCoords = (moved, tr, split) => {
|
||||||
|
const store = tr.doc.store
|
||||||
|
const startItem = moved.start.item
|
||||||
|
const endItem = moved.end.item
|
||||||
let start // this (inclusive) is the beginning of the moved area
|
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
|
let end // this (exclusive) is the first item after start that is not part of the moved area
|
||||||
if (moved.start.item) {
|
if (startItem) {
|
||||||
if (moved.start.assoc < 0) {
|
if (moved.start.assoc < 0) {
|
||||||
start = getItemCleanEnd(tr, moved.start.item)
|
// We know that the items have already been split, hence getItem suffices.
|
||||||
|
start = split ? getItemCleanEnd(tr, startItem) : getItem(store, startItem)
|
||||||
start = start.right
|
start = start.right
|
||||||
} else {
|
} else {
|
||||||
start = getItemCleanStart(tr, moved.start.item)
|
start = split ? getItemCleanStart(tr, startItem) : getItem(store, startItem)
|
||||||
}
|
}
|
||||||
} else if (moved.start.tname != null) {
|
} else if (moved.start.tname != null) {
|
||||||
start = tr.doc.get(moved.start.tname)._start
|
start = tr.doc.get(moved.start.tname)._start
|
||||||
} else if (moved.start.type) {
|
} else if (moved.start.type) {
|
||||||
start = /** @type {ContentType} */ (getItem(tr.doc.store, moved.start.type).content).type._start
|
start = /** @type {ContentType} */ (getItem(store, moved.start.type).content).type._start
|
||||||
} else {
|
} else {
|
||||||
error.unexpectedCase()
|
error.unexpectedCase()
|
||||||
}
|
}
|
||||||
if (moved.end.item) {
|
if (endItem) {
|
||||||
if (moved.end.assoc < 0) {
|
if (moved.end.assoc < 0) {
|
||||||
end = getItemCleanEnd(tr, moved.end.item)
|
end = split ? getItemCleanEnd(tr, endItem) : getItem(store, endItem)
|
||||||
end = end.right
|
end = end.right
|
||||||
} else {
|
} else {
|
||||||
end = getItemCleanStart(tr, moved.end.item)
|
end = split ? getItemCleanStart(tr, endItem) : getItem(store, endItem)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
end = null
|
error.unexpectedCase()
|
||||||
}
|
}
|
||||||
return { start: /** @type {Item} */ (start), end }
|
return { start: /** @type {Item} */ (start), end: /** @type {Item} */ (end) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo remove this if not needed
|
|
||||||
*
|
|
||||||
* @param {ContentMove} moved
|
|
||||||
* @param {Item} movedItem
|
|
||||||
* @param {Transaction} tr
|
* @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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ContentMove} moved
|
* @param {ContentMove} moved
|
||||||
* @param {Item} movedItem
|
* @param {Item} movedItem
|
||||||
* @param {Set<Item>} trackedMovedItems
|
* @param {Set<Item>} trackedMovedItems
|
||||||
* @param {Transaction} tr
|
|
||||||
* @return {boolean} true if there is a loop
|
* @return {boolean} true if there is a loop
|
||||||
*/
|
*/
|
||||||
export const findMoveLoop = (moved, movedItem, trackedMovedItems, tr) => {
|
export const findMoveLoop = (tr, moved, movedItem, trackedMovedItems) => {
|
||||||
if (trackedMovedItems.has(movedItem)) {
|
if (trackedMovedItems.has(movedItem)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -85,12 +65,15 @@ export const findMoveLoop = (moved, movedItem, trackedMovedItems, tr) => {
|
|||||||
/**
|
/**
|
||||||
* @type {{ start: Item | null, end: Item | null }}
|
* @type {{ start: Item | null, end: Item | null }}
|
||||||
*/
|
*/
|
||||||
let { start, end } = getMovedCoords(moved, tr)
|
let { start, end } = getMovedCoords(moved, tr, false)
|
||||||
while (start !== end && start != null) {
|
while (start !== end && start != null) {
|
||||||
if (start.deleted && start.moved === movedItem && start.content.constructor === ContentMove) {
|
if (
|
||||||
if (findMoveLoop(start.content, start, trackedMovedItems, tr)) {
|
!start.deleted &&
|
||||||
return true
|
start.moved === movedItem &&
|
||||||
}
|
start.content.constructor === ContentMove &&
|
||||||
|
findMoveLoop(tr, start.content, start, trackedMovedItems)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
start = start.right
|
start = start.right
|
||||||
}
|
}
|
||||||
@@ -172,33 +155,45 @@ export class ContentMove {
|
|||||||
* @param {Item} item
|
* @param {Item} item
|
||||||
*/
|
*/
|
||||||
integrate (transaction, item) {
|
integrate (transaction, item) {
|
||||||
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = []
|
const sm = /** @type {AbstractType<any>} */ (item.parent)._searchMarker
|
||||||
|
if (sm) sm.length = 0
|
||||||
|
const movedCoords = getMovedCoords(this, transaction, true)
|
||||||
/**
|
/**
|
||||||
* @type {{ start: Item | null, end: Item | null }}
|
* @type {{ start: Item | null, end: item | null }}
|
||||||
*/
|
*/
|
||||||
let { start, end } = getMovedCoords(this, transaction)
|
let { start, end } = movedCoords
|
||||||
let maxPriority = 0
|
let maxPriority = 0
|
||||||
// If this ContentMove was created locally, we set prio = -1. This indicates
|
// If this ContentMove was created locally, we set prio = -1. This indicates
|
||||||
// that we want to set prio to the current prio-maximum of the moved range.
|
// that we want to set prio to the current prio-maximum of the moved range.
|
||||||
const adaptPriority = this.priority < 0
|
const adaptPriority = this.priority < 0
|
||||||
while (start !== end && start != null) {
|
while (start !== end && start != null) {
|
||||||
if (!start.deleted) {
|
const prevMove = start.moved // this is the same as prevMove
|
||||||
const currMoved = start.moved
|
const nextPrio = prevMove ? /** @type {ContentMove} */ (prevMove.content).priority : -1
|
||||||
const nextPrio = currMoved ? /** @type {ContentMove} */ (currMoved.content).priority : -1
|
if (adaptPriority || nextPrio < this.priority || (prevMove != null && nextPrio === this.priority && (prevMove.id.client < item.id.client || (prevMove.id.client === item.id.client && prevMove.id.clock < item.id.clock)))) {
|
||||||
if (currMoved === null || adaptPriority || nextPrio < this.priority || currMoved.id.client < item.id.client || (currMoved.id.client === item.id.client && currMoved.id.clock < item.id.clock)) {
|
if (prevMove !== null) {
|
||||||
if (currMoved !== null) {
|
if (/** @type {ContentMove} */ (prevMove.content).isCollapsed()) {
|
||||||
this.overrides.add(currMoved)
|
prevMove.deleteAsCleanup(transaction, adaptPriority)
|
||||||
}
|
}
|
||||||
maxPriority = math.max(maxPriority, nextPrio)
|
this.overrides.add(prevMove)
|
||||||
// was already moved
|
if (start !== movedCoords.start) {
|
||||||
if (start.moved && !transaction.prevMoved.has(start)) {
|
// only add this to mergeStructs if this is not the first item
|
||||||
// we need to know which item previously moved an item
|
transaction._mergeStructs.push(start)
|
||||||
transaction.prevMoved.set(start, start.moved)
|
|
||||||
}
|
}
|
||||||
start.moved = item
|
|
||||||
} else {
|
|
||||||
/** @type {ContentMove} */ (currMoved.content).overrides.add(item)
|
|
||||||
}
|
}
|
||||||
|
maxPriority = math.max(maxPriority, nextPrio)
|
||||||
|
// was already moved
|
||||||
|
if (prevMove && !transaction.prevMoved.has(start) && !addsStruct(transaction, prevMove)) {
|
||||||
|
// 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
|
||||||
|
if (!start.deleted && start.content.constructor === ContentMove && findMoveLoop(transaction, start.content, start, new Set([item]))) {
|
||||||
|
item.deleteAsCleanup(transaction, adaptPriority)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (prevMove != null) {
|
||||||
|
/** @type {ContentMove} */ (prevMove.content).overrides.add(item)
|
||||||
}
|
}
|
||||||
start = start.right
|
start = start.right
|
||||||
}
|
}
|
||||||
@@ -215,9 +210,19 @@ export class ContentMove {
|
|||||||
/**
|
/**
|
||||||
* @type {{ start: Item | null, end: Item | null }}
|
* @type {{ start: Item | null, end: Item | null }}
|
||||||
*/
|
*/
|
||||||
let { start, end } = getMovedCoords(this, transaction)
|
let { start, end } = getMovedCoords(this, transaction, false)
|
||||||
while (start !== end && start != null) {
|
while (start !== end && start != null) {
|
||||||
if (start.moved === item) {
|
if (start.moved === item) {
|
||||||
|
const prevMoved = transaction.prevMoved.get(start)
|
||||||
|
if (addsStruct(transaction, item)) {
|
||||||
|
if (prevMoved === item) {
|
||||||
|
// Edge case: Item has been moved by this move op and it has been created & deleted in the same transaction (hence no effect that should be emitted by the change computation)
|
||||||
|
transaction.prevMoved.delete(start)
|
||||||
|
}
|
||||||
|
} else if (prevMoved == null) { // && !addsStruct(tr, item)
|
||||||
|
// Normal case: item has been moved by this move and it has not been created & deleted in the same transaction
|
||||||
|
transaction.prevMoved.set(start, item)
|
||||||
|
}
|
||||||
start.moved = null
|
start.moved = null
|
||||||
}
|
}
|
||||||
start = start.right
|
start = start.right
|
||||||
@@ -227,11 +232,14 @@ export class ContentMove {
|
|||||||
*/
|
*/
|
||||||
const reIntegrate = reIntegrateItem => {
|
const reIntegrate = reIntegrateItem => {
|
||||||
const content = /** @type {ContentMove} */ (reIntegrateItem.content)
|
const content = /** @type {ContentMove} */ (reIntegrateItem.content)
|
||||||
if (reIntegrateItem.deleted) {
|
// content is not yet transformed to a ContentDeleted
|
||||||
// potentially we can integrate the items that reIntegrateItem overrides
|
if (content.getRef() === 11) {
|
||||||
content.overrides.forEach(reIntegrate)
|
if (reIntegrateItem.deleted) {
|
||||||
} else {
|
// potentially we can integrate the items that reIntegrateItem overrides
|
||||||
content.integrate(transaction, reIntegrateItem)
|
content.overrides.forEach(reIntegrate)
|
||||||
|
} else {
|
||||||
|
content.integrate(transaction, reIntegrateItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.overrides.forEach(reIntegrate)
|
this.overrides.forEach(reIntegrate)
|
||||||
@@ -248,12 +256,11 @@ export class ContentMove {
|
|||||||
*/
|
*/
|
||||||
write (encoder, offset) {
|
write (encoder, offset) {
|
||||||
const isCollapsed = this.isCollapsed()
|
const isCollapsed = this.isCollapsed()
|
||||||
encoding.writeUint8(encoder.restEncoder, isCollapsed ? 1 : 0)
|
encoding.writeVarUint(encoder.restEncoder, (isCollapsed ? 1 : 0) | (this.start.assoc >= 0 ? 2 : 0) | (this.end.assoc >= 0 ? 4 : 0) | this.priority << 6)
|
||||||
encoder.writeBuf(encodeRelativePosition(this.start))
|
writeID(encoder.restEncoder, /** @type {ID} */ (this.start.item))
|
||||||
if (!isCollapsed) {
|
if (!isCollapsed) {
|
||||||
encoder.writeBuf(encodeRelativePosition(this.end))
|
writeID(encoder.restEncoder, /** @type {ID} */ (this.end.item))
|
||||||
}
|
}
|
||||||
encoding.writeVarUint(encoder.restEncoder, this.priority)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,17 +277,20 @@ export class ContentMove {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @todo use binary encoding option for start & end relpos's
|
|
||||||
*
|
*
|
||||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
* @return {ContentMove}
|
* @return {ContentMove}
|
||||||
*/
|
*/
|
||||||
export const readContentMove = decoder => {
|
export const readContentMove = decoder => {
|
||||||
const isCollapsed = decoding.readUint8(decoder.restDecoder) === 1
|
const info = decoding.readVarUint(decoder.restDecoder)
|
||||||
const start = decodeRelativePosition(decoder.readBuf())
|
const isCollapsed = (info & 1) === 1
|
||||||
const end = isCollapsed ? start.clone() : decodeRelativePosition(decoder.readBuf())
|
const startAssoc = (info & 2) === 2 ? 0 : -1
|
||||||
if (isCollapsed) {
|
const endAssoc = (info & 4) === 4 ? 0 : -1
|
||||||
end.assoc = -1
|
// @TODO use BIT3 & BIT4 to indicate the case `null` is the start/end
|
||||||
}
|
// BIT5 is reserved for future extensions
|
||||||
return new ContentMove(start, end, decoding.readVarUint(decoder.restDecoder))
|
const priority = info >>> 6
|
||||||
|
const startId = readID(decoder.restDecoder)
|
||||||
|
const start = new RelativePosition(null, null, startId, startAssoc)
|
||||||
|
const end = new RelativePosition(null, null, isCollapsed ? startId : readID(decoder.restDecoder), endAssoc)
|
||||||
|
return new ContentMove(start, end, priority)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as binary from 'lib0/binary'
|
import * as binary from 'lib0/binary'
|
||||||
|
import { ContentMove } from './ContentMove.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo This should return several items
|
* @todo This should return several items
|
||||||
@@ -118,6 +119,7 @@ export const splitItem = (transaction, leftItem, diff) => {
|
|||||||
}
|
}
|
||||||
leftItem.length = diff
|
leftItem.length = diff
|
||||||
if (leftItem.moved) {
|
if (leftItem.moved) {
|
||||||
|
rightItem.moved = leftItem.moved
|
||||||
const m = transaction.prevMoved.get(leftItem)
|
const m = transaction.prevMoved.get(leftItem)
|
||||||
if (m) {
|
if (m) {
|
||||||
transaction.prevMoved.set(rightItem, m)
|
transaction.prevMoved.set(rightItem, m)
|
||||||
@@ -381,9 +383,19 @@ export class Item extends AbstractStruct {
|
|||||||
if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) {
|
if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) {
|
||||||
return this.parent.client
|
return this.parent.client
|
||||||
}
|
}
|
||||||
|
if (this.content.constructor === ContentMove) {
|
||||||
|
const c = /** @type {ContentMove} */ (this.content)
|
||||||
|
const start = c.start.item
|
||||||
|
const end = c.isCollapsed() ? null : c.end.item
|
||||||
|
if (start && start.clock >= getState(store, start.client)) {
|
||||||
|
return start.client
|
||||||
|
}
|
||||||
|
if (end && end.clock >= getState(store, end.client)) {
|
||||||
|
return end.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We have all missing ids, now find the items
|
// We have all missing ids, now find the items
|
||||||
|
|
||||||
if (this.origin) {
|
if (this.origin) {
|
||||||
this.left = getItemCleanEnd(transaction, this.origin)
|
this.left = getItemCleanEnd(transaction, this.origin)
|
||||||
this.origin = this.left.lastId
|
this.origin = this.left.lastId
|
||||||
@@ -413,6 +425,7 @@ export class Item extends AbstractStruct {
|
|||||||
this.parent = /** @type {ContentType} */ (parentItem.content).type
|
this.parent = /** @type {ContentType} */ (parentItem.content).type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,6 +535,24 @@ export class Item extends AbstractStruct {
|
|||||||
if (this.parentSub === null && this.countable && !this.deleted) {
|
if (this.parentSub === null && this.countable && !this.deleted) {
|
||||||
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
|
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
|
||||||
}
|
}
|
||||||
|
// check if this item is in a moved range
|
||||||
|
if ((this.left && this.left.moved) || (this.right && this.right.moved)) {
|
||||||
|
const leftMoved = this.left && this.left.moved && /** @type {ContentMove} */ (this.left.moved.content)
|
||||||
|
const rightMoved = this.right && this.right.moved && /** @type {ContentMove} */ (this.right.moved.content)
|
||||||
|
if (leftMoved === rightMoved) {
|
||||||
|
this.moved = /** @type {Item} */ (this.left).moved
|
||||||
|
} else if (
|
||||||
|
(leftMoved != null && !leftMoved.isCollapsed()) ||
|
||||||
|
(rightMoved != null && !rightMoved.isCollapsed())
|
||||||
|
) {
|
||||||
|
// We know that this item is on the edge of a moved range.
|
||||||
|
// @todo Instead, we could check to which moved-range this item belongs
|
||||||
|
// This approach (reintegration) is pretty expensive in some scenarios
|
||||||
|
leftMoved && leftMoved.integrate(transaction, /** @type {any} */ (this.left).moved)
|
||||||
|
rightMoved && rightMoved.integrate(transaction, /** @type {any} */ (this.right).moved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addStruct(transaction.doc.store, this)
|
addStruct(transaction.doc.store, this)
|
||||||
this.content.integrate(transaction, this)
|
this.content.integrate(transaction, this)
|
||||||
// add parent to transaction.changed
|
// add parent to transaction.changed
|
||||||
@@ -632,6 +663,22 @@ export class Item extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to `this.delete(tr)`, but additionally ensures
|
||||||
|
* that the deleted range is broadcasted using a different
|
||||||
|
* origin/source in a separate update event, so that
|
||||||
|
* the providers don't filter this message.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {boolean} isLocal
|
||||||
|
*/
|
||||||
|
deleteAsCleanup (transaction, isLocal) {
|
||||||
|
this.delete(transaction)
|
||||||
|
if (!isLocal) {
|
||||||
|
addToDeleteSet(transaction.cleanupDeletions, this.id.client, this.id.clock, this.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StructStore} store
|
* @param {StructStore} store
|
||||||
* @param {boolean} parentGCd
|
* @param {boolean} parentGCd
|
||||||
@@ -715,7 +762,7 @@ export const readItemContent = (decoder, info) => contentRefs[info & binary.BITS
|
|||||||
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
|
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
|
||||||
*/
|
*/
|
||||||
export const contentRefs = [
|
export const contentRefs = [
|
||||||
() => { error.unexpectedCase() }, // GC is not ItemContent
|
error.unexpectedCase, // GC is not ItemContent
|
||||||
readContentDeleted, // 1
|
readContentDeleted, // 1
|
||||||
readContentJSON, // 2
|
readContentJSON, // 2
|
||||||
readContentBinary, // 3
|
readContentBinary, // 3
|
||||||
@@ -725,7 +772,7 @@ export const contentRefs = [
|
|||||||
readContentType, // 7
|
readContentType, // 7
|
||||||
readContentAny, // 8
|
readContentAny, // 8
|
||||||
readContentDoc, // 9
|
readContentDoc, // 9
|
||||||
() => { error.unexpectedCase() }, // 10 - Skip is not ItemContent
|
error.unexpectedCase, // 10 - Skip is not ItemContent
|
||||||
readContentMove // 11
|
readContentMove // 11
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
createID,
|
createID,
|
||||||
ContentAny,
|
ContentAny,
|
||||||
ContentBinary,
|
ContentBinary,
|
||||||
ListIterator,
|
ListCursor,
|
||||||
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
ContentDoc, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as map from 'lib0/map'
|
import * as map from 'lib0/map'
|
||||||
@@ -19,7 +19,8 @@ import * as iterator from 'lib0/iterator'
|
|||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
|
|
||||||
const maxSearchMarker = 80
|
const maxSearchMarker = 300
|
||||||
|
const freshSearchMarkerDistance = 30
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search marker help us to find positions in the associative array faster.
|
* Search marker help us to find positions in the associative array faster.
|
||||||
@@ -32,24 +33,25 @@ const maxSearchMarker = 80
|
|||||||
* @param {Transaction} tr
|
* @param {Transaction} tr
|
||||||
* @param {AbstractType<any>} yarray
|
* @param {AbstractType<any>} yarray
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
* @param {function(ListIterator):T} f
|
* @param {function(ListCursor):T} f
|
||||||
|
* @return T
|
||||||
*/
|
*/
|
||||||
export const useSearchMarker = (tr, yarray, index, f) => {
|
export const useSearchMarker = (tr, yarray, index, f) => {
|
||||||
const searchMarker = yarray._searchMarker
|
const searchMarker = yarray._searchMarker
|
||||||
if (searchMarker === null || yarray._start === null || index < 5) {
|
if (searchMarker === null || yarray._start === null || index < freshSearchMarkerDistance) {
|
||||||
return f(new ListIterator(yarray).forward(tr, index))
|
return f(new ListCursor(yarray).forward(tr, index, true))
|
||||||
}
|
}
|
||||||
if (searchMarker.length === 0) {
|
if (searchMarker.length === 0) {
|
||||||
const sm = new ListIterator(yarray).forward(tr, index)
|
const sm = new ListCursor(yarray).forward(tr, index, true)
|
||||||
searchMarker.push(sm)
|
searchMarker.push(sm)
|
||||||
if (sm.nextItem) sm.nextItem.marker = true
|
if (sm.nextItem) sm.nextItem.marker = true
|
||||||
}
|
}
|
||||||
const sm = searchMarker.reduce(
|
const sm = searchMarker.reduce(
|
||||||
(a, b, arrayIndex) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b
|
(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
|
||||||
const createFreshMarker = searchMarker.length < maxSearchMarker && (math.abs(sm.index - index) > 5 || newIsCheaper)
|
const createFreshMarker = searchMarker.length < maxSearchMarker && (math.abs(sm.index - index) > freshSearchMarkerDistance || newIsCheaper)
|
||||||
const fsm = createFreshMarker ? (newIsCheaper ? new ListIterator(yarray) : sm.clone()) : sm
|
const fsm = createFreshMarker ? (newIsCheaper ? new ListCursor(yarray) : sm.clone()) : sm
|
||||||
const prevItem = /** @type {Item} */ (sm.nextItem)
|
const prevItem = /** @type {Item} */ (sm.nextItem)
|
||||||
if (createFreshMarker) {
|
if (createFreshMarker) {
|
||||||
searchMarker.push(fsm)
|
searchMarker.push(fsm)
|
||||||
@@ -58,16 +60,8 @@ export const useSearchMarker = (tr, yarray, index, f) => {
|
|||||||
if (diff > 0) {
|
if (diff > 0) {
|
||||||
fsm.backward(tr, diff)
|
fsm.backward(tr, diff)
|
||||||
} else {
|
} else {
|
||||||
fsm.forward(tr, -diff)
|
fsm.forward(tr, -diff, true)
|
||||||
}
|
}
|
||||||
// @todo remove this tests
|
|
||||||
/*
|
|
||||||
const otherTesting = new ListIterator(yarray)
|
|
||||||
otherTesting.forward(tr, index)
|
|
||||||
if (otherTesting.nextItem !== fsm.nextItem || otherTesting.index !== fsm.index || otherTesting.reachedEnd !== fsm.reachedEnd) {
|
|
||||||
throw new Error('udtirane')
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const result = f(fsm)
|
const result = f(fsm)
|
||||||
if (fsm.reachedEnd) {
|
if (fsm.reachedEnd) {
|
||||||
fsm.reachedEnd = false
|
fsm.reachedEnd = false
|
||||||
@@ -77,6 +71,8 @@ export const useSearchMarker = (tr, yarray, index, f) => {
|
|||||||
}
|
}
|
||||||
fsm.rel = 0
|
fsm.rel = 0
|
||||||
}
|
}
|
||||||
|
fsm.index -= fsm.rel
|
||||||
|
fsm.rel = 0
|
||||||
if (!createFreshMarker) {
|
if (!createFreshMarker) {
|
||||||
// reused old marker and we moved to a different position
|
// reused old marker and we moved to a different position
|
||||||
prevItem.marker = false
|
prevItem.marker = false
|
||||||
@@ -98,10 +94,10 @@ export const useSearchMarker = (tr, yarray, index, f) => {
|
|||||||
*
|
*
|
||||||
* This should be called before doing a deletion!
|
* This should be called before doing a deletion!
|
||||||
*
|
*
|
||||||
* @param {Array<ListIterator>} searchMarker
|
* @param {Array<ListCursor>} searchMarker
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
* @param {number} len If insertion, len is positive. If deletion, len is negative.
|
* @param {number} len If insertion, len is positive. If deletion, len is negative.
|
||||||
* @param {ListIterator|null} origSearchMarker Do not update this searchmarker because it is the one we used to manipulate. @todo !=null for improved perf in ytext
|
* @param {ListCursor|null} origSearchMarker Do not update this searchmarker because it is the one we used to manipulate. @todo !=null for improved perf in ytext
|
||||||
*/
|
*/
|
||||||
export const updateMarkerChanges = (searchMarker, index, len, origSearchMarker) => {
|
export const updateMarkerChanges = (searchMarker, index, len, origSearchMarker) => {
|
||||||
for (let i = searchMarker.length - 1; i >= 0; i--) {
|
for (let i = searchMarker.length - 1; i >= 0; i--) {
|
||||||
@@ -194,7 +190,7 @@ export class AbstractType {
|
|||||||
*/
|
*/
|
||||||
this._dEH = createEventHandler()
|
this._dEH = createEventHandler()
|
||||||
/**
|
/**
|
||||||
* @type {null | Array<ListIterator>}
|
* @type {null | Array<ListCursor>}
|
||||||
*/
|
*/
|
||||||
this._searchMarker = null
|
this._searchMarker = null
|
||||||
/**
|
/**
|
||||||
@@ -373,146 +369,6 @@ export const typeListToArray = type => {
|
|||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {AbstractType<any>} type
|
|
||||||
* @param {Snapshot} snapshot
|
|
||||||
* @return {Array<any>}
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export const typeListToArraySnapshot = (type, snapshot) => {
|
|
||||||
const cs = []
|
|
||||||
let n = type._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (n.countable && isVisible(n, snapshot)) {
|
|
||||||
const c = n.content.getContent()
|
|
||||||
for (let i = 0; i < c.length; i++) {
|
|
||||||
cs.push(c[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n = n.right
|
|
||||||
}
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a provided function on once on overy element of this YArray.
|
|
||||||
*
|
|
||||||
* @param {AbstractType<any>} type
|
|
||||||
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export const typeListForEach = (type, f) => {
|
|
||||||
let index = 0
|
|
||||||
let n = type._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (n.countable && !n.deleted) {
|
|
||||||
const c = n.content.getContent()
|
|
||||||
for (let i = 0; i < c.length; i++) {
|
|
||||||
f(c[i], index++, type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n = n.right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template C,R
|
|
||||||
* @param {AbstractType<any>} type
|
|
||||||
* @param {function(C,number,AbstractType<any>):R} f
|
|
||||||
* @return {Array<R>}
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export const typeListMap = (type, f) => {
|
|
||||||
/**
|
|
||||||
* @type {Array<any>}
|
|
||||||
*/
|
|
||||||
const result = []
|
|
||||||
typeListForEach(type, (c, i) => {
|
|
||||||
result.push(f(c, i, type))
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {AbstractType<any>} type
|
|
||||||
* @return {IterableIterator<any>}
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export const typeListCreateIterator = type => {
|
|
||||||
let n = type._start
|
|
||||||
/**
|
|
||||||
* @type {Array<any>|null}
|
|
||||||
*/
|
|
||||||
let currentContent = null
|
|
||||||
let currentContentIndex = 0
|
|
||||||
return {
|
|
||||||
[Symbol.iterator] () {
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
next: () => {
|
|
||||||
// find some content
|
|
||||||
if (currentContent === null) {
|
|
||||||
while (n !== null && n.deleted) {
|
|
||||||
n = n.right
|
|
||||||
}
|
|
||||||
// check if we reached the end, no need to check currentContent, because it does not exist
|
|
||||||
if (n === null) {
|
|
||||||
return {
|
|
||||||
done: true,
|
|
||||||
value: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// we found n, so we can set currentContent
|
|
||||||
currentContent = n.content.getContent()
|
|
||||||
currentContentIndex = 0
|
|
||||||
n = n.right // we used the content of n, now iterate to next
|
|
||||||
}
|
|
||||||
const value = currentContent[currentContentIndex++]
|
|
||||||
// check if we need to empty currentContent
|
|
||||||
if (currentContent.length <= currentContentIndex) {
|
|
||||||
currentContent = null
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
done: false,
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes a provided function on once on overy element of this YArray.
|
|
||||||
* Operates on a snapshotted state of the document.
|
|
||||||
*
|
|
||||||
* @param {AbstractType<any>} type
|
|
||||||
* @param {function(any,number,AbstractType<any>):void} f A function to execute on every element of this YArray.
|
|
||||||
* @param {Snapshot} snapshot
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @function
|
|
||||||
*/
|
|
||||||
export const typeListForEachSnapshot = (type, f, snapshot) => {
|
|
||||||
let index = 0
|
|
||||||
let n = type._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (n.countable && isVisible(n, snapshot)) {
|
|
||||||
const c = n.content.getContent()
|
|
||||||
for (let i = 0; i < c.length; i++) {
|
|
||||||
f(c[i], index++, type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n = n.right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import {
|
|||||||
YArrayRefID,
|
YArrayRefID,
|
||||||
callTypeObservers,
|
callTypeObservers,
|
||||||
transact,
|
transact,
|
||||||
ListIterator,
|
ListCursor,
|
||||||
useSearchMarker,
|
useSearchMarker,
|
||||||
createRelativePositionFromTypeIndex,
|
createRelativePositionFromTypeIndex,
|
||||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line
|
||||||
|
getMinimalListViewRanges
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +45,7 @@ export class YArray extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
this._prelimContent = []
|
this._prelimContent = []
|
||||||
/**
|
/**
|
||||||
* @type {Array<ListIterator>}
|
* @type {Array<ListCursor>}
|
||||||
*/
|
*/
|
||||||
this._searchMarker = []
|
this._searchMarker = []
|
||||||
}
|
}
|
||||||
@@ -140,7 +141,15 @@ export class YArray extends AbstractType {
|
|||||||
/**
|
/**
|
||||||
* Move a single item from $index to $target.
|
* Move a single item from $index to $target.
|
||||||
*
|
*
|
||||||
* @todo make sure that collapsed moves are removed (i.e. when moving the same item twice)
|
* If the original item is to the left of $target, then the index of the item will decrement.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* yarray.insert(0, [1, 2, 3])
|
||||||
|
* yarray.move(0, 3) // move "1" to index 3
|
||||||
|
* yarray.toArray() // => [2, 3, 1]
|
||||||
|
* yarray.move(2, 0) // move "1" to index 0
|
||||||
|
* yarray.toArray() // => [1, 2, 3]
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
* @param {number} target
|
* @param {number} target
|
||||||
@@ -152,11 +161,11 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
transact(this.doc, transaction => {
|
transact(this.doc, transaction => {
|
||||||
const left = createRelativePositionFromTypeIndex(this, index, 1)
|
const start = createRelativePositionFromTypeIndex(this, index, 1)
|
||||||
const right = left.clone()
|
const end = start.clone()
|
||||||
right.assoc = -1
|
end.assoc = -1
|
||||||
useSearchMarker(transaction, this, target, walker => {
|
useSearchMarker(transaction, this, target, walker => {
|
||||||
walker.insertMove(transaction, left, right)
|
walker.insertMove(transaction, [{ start, end }])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -166,27 +175,32 @@ export class YArray extends AbstractType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} start Inclusive move-start
|
* @experimental
|
||||||
* @param {number} end Inclusive move-end
|
*
|
||||||
|
* @param {number} startIndex Inclusive move-start
|
||||||
|
* @param {number} endIndex Inclusive move-end
|
||||||
* @param {number} target
|
* @param {number} target
|
||||||
* @param {number} assocStart >=0 if start should be associated with the right character. See relative-position assoc parameter.
|
* @param {number} assocStart >=0 if start should be associated with the right character. See relative-position assoc parameter.
|
||||||
* @param {number} assocEnd >= 0 if end should be associated with the right character.
|
* @param {number} assocEnd >= 0 if end should be associated with the right character.
|
||||||
*/
|
*/
|
||||||
moveRange (start, end, target, assocStart = 1, assocEnd = -1) {
|
moveRange (startIndex, endIndex, target, assocStart = 1, assocEnd = -1) {
|
||||||
if (start <= target && target <= end) {
|
if (
|
||||||
// It doesn't make sense to move a range into the same range (it's basically a no-op).
|
(startIndex <= target && target <= endIndex) || // It doesn't make sense to move a range into the same range (it's basically a no-op).
|
||||||
|
endIndex - startIndex < 0 // require length of >= 0
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
transact(this.doc, transaction => {
|
transact(this.doc, transaction => {
|
||||||
const left = createRelativePositionFromTypeIndex(this, start, assocStart)
|
const ranges = useSearchMarker(transaction, this, startIndex, walker =>
|
||||||
const right = createRelativePositionFromTypeIndex(this, end + 1, assocEnd)
|
getMinimalListViewRanges(transaction, walker, endIndex - startIndex + 1)
|
||||||
|
)
|
||||||
useSearchMarker(transaction, this, target, walker => {
|
useSearchMarker(transaction, this, target, walker => {
|
||||||
walker.insertMove(transaction, left, right)
|
walker.insertMove(transaction, ranges)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const content = /** @type {Array<any>} */ (this._prelimContent).splice(start, end - start + 1)
|
const content = /** @type {Array<any>} */ (this._prelimContent).splice(startIndex, endIndex - startIndex + 1)
|
||||||
;/** @type {Array<any>} */ (this._prelimContent).splice(target, 0, ...content)
|
;/** @type {Array<any>} */ (this._prelimContent).splice(target, 0, ...content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +262,7 @@ export class YArray extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
toArray () {
|
toArray () {
|
||||||
return transact(/** @type {Doc} */ (this.doc), tr =>
|
return transact(/** @type {Doc} */ (this.doc), tr =>
|
||||||
new ListIterator(this).slice(tr, this.length)
|
new ListCursor(this).slice(tr, this.length)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +301,7 @@ export class YArray extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
map (f) {
|
map (f) {
|
||||||
return transact(/** @type {Doc} */ (this.doc), tr =>
|
return transact(/** @type {Doc} */ (this.doc), tr =>
|
||||||
new ListIterator(this).map(tr, f)
|
new ListCursor(this).map(tr, f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +312,7 @@ export class YArray extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
forEach (f) {
|
forEach (f) {
|
||||||
return transact(/** @type {Doc} */ (this.doc), tr =>
|
return transact(/** @type {Doc} */ (this.doc), tr =>
|
||||||
new ListIterator(this).forEach(tr, f)
|
new ListCursor(this).forEach(tr, f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +320,7 @@ export class YArray extends AbstractType {
|
|||||||
* @return {IterableIterator<T>}
|
* @return {IterableIterator<T>}
|
||||||
*/
|
*/
|
||||||
[Symbol.iterator] () {
|
[Symbol.iterator] () {
|
||||||
|
// @todo, this could be optimized using a real iterator
|
||||||
return this.toArray().values()
|
return this.toArray().values()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
ContentType,
|
ContentType,
|
||||||
useSearchMarker,
|
useSearchMarker,
|
||||||
findIndexCleanStart,
|
findIndexCleanStart,
|
||||||
ListIterator, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
ListCursor, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as object from 'lib0/object'
|
import * as object from 'lib0/object'
|
||||||
@@ -785,7 +785,7 @@ export class YText extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
||||||
/**
|
/**
|
||||||
* @type {Array<ListIterator>}
|
* @type {Array<ListCursor>}
|
||||||
*/
|
*/
|
||||||
this._searchMarker = []
|
this._searchMarker = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
typeMapSet,
|
typeMapSet,
|
||||||
typeMapGet,
|
typeMapGet,
|
||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
typeListForEach,
|
|
||||||
YXmlElementRefID,
|
YXmlElementRefID,
|
||||||
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Snapshot, Doc, Item // eslint-disable-line
|
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Snapshot, Doc, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
@@ -185,36 +184,6 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
return typeMapGetAll(this)
|
return typeMapGetAll(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Dom Element that mirrors this YXmlElement.
|
|
||||||
*
|
|
||||||
* @param {Document} [_document=document] The document object (you must define
|
|
||||||
* this when calling this method in
|
|
||||||
* nodejs)
|
|
||||||
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
|
|
||||||
* are presented in the DOM
|
|
||||||
* @param {any} [binding] You should not set this property. This is
|
|
||||||
* used if DomBinding wants to create a
|
|
||||||
* association to the created DOM type.
|
|
||||||
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
toDOM (_document = document, hooks = {}, binding) {
|
|
||||||
const dom = _document.createElement(this.nodeName)
|
|
||||||
const attrs = this.getAttributes()
|
|
||||||
for (const key in attrs) {
|
|
||||||
dom.setAttribute(key, attrs[key])
|
|
||||||
}
|
|
||||||
typeListForEach(this, yxml => {
|
|
||||||
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
|
||||||
})
|
|
||||||
if (binding !== undefined) {
|
|
||||||
binding._createAssociation(dom, this)
|
|
||||||
}
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the properties of this type to binary and write it to an
|
* Transform the properties of this type to binary and write it to an
|
||||||
* BinaryEncoder.
|
* BinaryEncoder.
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
YXmlEvent,
|
YXmlEvent,
|
||||||
YXmlElement,
|
YXmlElement,
|
||||||
AbstractType,
|
AbstractType,
|
||||||
typeListMap,
|
|
||||||
typeListForEach,
|
|
||||||
typeListInsertGenericsAfter,
|
typeListInsertGenericsAfter,
|
||||||
typeListToArray,
|
typeListToArray,
|
||||||
YXmlFragmentRefID,
|
YXmlFragmentRefID,
|
||||||
@@ -15,7 +13,8 @@ import {
|
|||||||
transact,
|
transact,
|
||||||
typeListSlice,
|
typeListSlice,
|
||||||
useSearchMarker,
|
useSearchMarker,
|
||||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot, // eslint-disable-line
|
||||||
|
ListCursor
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error'
|
import * as error from 'lib0/error'
|
||||||
@@ -254,7 +253,10 @@ export class YXmlFragment extends AbstractType {
|
|||||||
* @return {string} The string representation of all children.
|
* @return {string} The string representation of all children.
|
||||||
*/
|
*/
|
||||||
toString () {
|
toString () {
|
||||||
return typeListMap(this, xml => xml.toString()).join('')
|
if (this.doc != null) {
|
||||||
|
return transact(this.doc, tr => new ListCursor(this).map(tr, xml => xml.toString()).join(''))
|
||||||
|
}
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,32 +266,6 @@ export class YXmlFragment extends AbstractType {
|
|||||||
return this.toString()
|
return this.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Dom Element that mirrors this YXmlElement.
|
|
||||||
*
|
|
||||||
* @param {Document} [_document=document] The document object (you must define
|
|
||||||
* this when calling this method in
|
|
||||||
* nodejs)
|
|
||||||
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
|
|
||||||
* are presented in the DOM
|
|
||||||
* @param {any} [binding] You should not set this property. This is
|
|
||||||
* used if DomBinding wants to create a
|
|
||||||
* association to the created DOM type.
|
|
||||||
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
toDOM (_document = document, hooks = {}, binding) {
|
|
||||||
const fragment = _document.createDocumentFragment()
|
|
||||||
if (binding !== undefined) {
|
|
||||||
binding._createAssociation(fragment, this)
|
|
||||||
}
|
|
||||||
typeListForEach(this, xmlType => {
|
|
||||||
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null)
|
|
||||||
})
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts new content at an index.
|
* Inserts new content at an index.
|
||||||
*
|
*
|
||||||
@@ -302,7 +278,7 @@ export class YXmlFragment extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
insert (index, content) {
|
insert (index, content) {
|
||||||
if (this.doc !== null) {
|
if (this.doc !== null) {
|
||||||
return transact(/** @type {Doc} */ (this.doc), transaction =>
|
return transact(this.doc, transaction =>
|
||||||
useSearchMarker(transaction, this, index, walker =>
|
useSearchMarker(transaction, this, index, walker =>
|
||||||
walker.insertArrayValue(transaction, content)
|
walker.insertArrayValue(transaction, content)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,36 +40,6 @@ export class YXmlHook extends YMap {
|
|||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Dom Element that mirrors this YXmlElement.
|
|
||||||
*
|
|
||||||
* @param {Document} [_document=document] The document object (you must define
|
|
||||||
* this when calling this method in
|
|
||||||
* nodejs)
|
|
||||||
* @param {Object.<string, any>} [hooks] Optional property to customize how hooks
|
|
||||||
* are presented in the DOM
|
|
||||||
* @param {any} [binding] You should not set this property. This is
|
|
||||||
* used if DomBinding wants to create a
|
|
||||||
* association to the created DOM type
|
|
||||||
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
toDOM (_document = document, hooks = {}, binding) {
|
|
||||||
const hook = hooks[this.hookName]
|
|
||||||
let dom
|
|
||||||
if (hook !== undefined) {
|
|
||||||
dom = hook.createDom(this)
|
|
||||||
} else {
|
|
||||||
dom = document.createElement(this.hookName)
|
|
||||||
}
|
|
||||||
dom.setAttribute('data-yjs-hook', this.hookName)
|
|
||||||
if (binding !== undefined) {
|
|
||||||
binding._createAssociation(dom, this)
|
|
||||||
}
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the properties of this type to binary and write it to an
|
* Transform the properties of this type to binary and write it to an
|
||||||
* BinaryEncoder.
|
* BinaryEncoder.
|
||||||
|
|||||||
@@ -39,29 +39,6 @@ export class YXmlText extends YText {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Dom Element that mirrors this YXmlText.
|
|
||||||
*
|
|
||||||
* @param {Document} [_document=document] The document object (you must define
|
|
||||||
* this when calling this method in
|
|
||||||
* nodejs)
|
|
||||||
* @param {Object<string, any>} [hooks] Optional property to customize how hooks
|
|
||||||
* are presented in the DOM
|
|
||||||
* @param {any} [binding] You should not set this property. This is
|
|
||||||
* used if DomBinding wants to create a
|
|
||||||
* association to the created DOM type.
|
|
||||||
* @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
toDOM (_document = document, hooks, binding) {
|
|
||||||
const dom = _document.createTextNode(this.toString())
|
|
||||||
if (binding !== undefined) {
|
|
||||||
binding._createAssociation(dom, this)
|
|
||||||
}
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
|
|
||||||
toString () {
|
toString () {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return this.toDelta().map(delta => {
|
return this.toDelta().map(delta => {
|
||||||
|
|||||||
740
src/utils/ListCursor.js
Normal file
740
src/utils/ListCursor.js
Normal file
@@ -0,0 +1,740 @@
|
|||||||
|
import * as error from 'lib0/error'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getItemCleanStart,
|
||||||
|
createID,
|
||||||
|
getMovedCoords,
|
||||||
|
updateMarkerChanges,
|
||||||
|
getState,
|
||||||
|
ContentAny,
|
||||||
|
ContentBinary,
|
||||||
|
ContentType,
|
||||||
|
ContentDoc,
|
||||||
|
Doc,
|
||||||
|
compareIDs,
|
||||||
|
createRelativePosition,
|
||||||
|
RelativePosition, ID, AbstractContent, ContentMove, Transaction, Item, AbstractType // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
import { compareRelativePositions } from './RelativePosition.js'
|
||||||
|
import * as array from 'lib0/array'
|
||||||
|
|
||||||
|
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 {ListCursor} 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, false)
|
||||||
|
start = coords.start
|
||||||
|
end = coords.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li.currMove = move
|
||||||
|
li.currMoveStart = start
|
||||||
|
li.currMoveEnd = end
|
||||||
|
li.reachedEnd = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure that helps to iterate through list-like structures. This is a useful abstraction that keeps track of move operations.
|
||||||
|
*/
|
||||||
|
export class ListCursor {
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
*/
|
||||||
|
constructor (type) {
|
||||||
|
this.type = type
|
||||||
|
/**
|
||||||
|
* Current index-position
|
||||||
|
*/
|
||||||
|
this.index = 0
|
||||||
|
/**
|
||||||
|
* Relative position to the current item (if item.content.length > 1)
|
||||||
|
*/
|
||||||
|
this.rel = 0
|
||||||
|
/**
|
||||||
|
* This refers to the current right item, unless reachedEnd is true. Then it refers to the left item.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.nextItem = type._start
|
||||||
|
this.reachedEnd = type._start === null
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.currMove = null
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.currMoveStart = null
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.currMoveEnd = null
|
||||||
|
/**
|
||||||
|
* @type {Array<{ start: Item | null, end: Item | null, move: Item }>}
|
||||||
|
*/
|
||||||
|
this.movedStack = []
|
||||||
|
}
|
||||||
|
|
||||||
|
clone () {
|
||||||
|
const iter = new ListCursor(this.type)
|
||||||
|
iter.index = this.index
|
||||||
|
iter.rel = this.rel
|
||||||
|
iter.nextItem = this.nextItem
|
||||||
|
iter.reachedEnd = this.reachedEnd
|
||||||
|
iter.currMove = this.currMove
|
||||||
|
iter.currMoveStart = this.currMoveStart
|
||||||
|
iter.currMoveEnd = this.currMoveEnd
|
||||||
|
iter.movedStack = this.movedStack.slice()
|
||||||
|
return iter
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
get left () {
|
||||||
|
if (this.reachedEnd) {
|
||||||
|
return this.nextItem
|
||||||
|
} else {
|
||||||
|
return this.nextItem && this.nextItem.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
get right () {
|
||||||
|
if (this.reachedEnd) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return this.nextItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
moveTo (tr, index) {
|
||||||
|
const diff = index - this.index
|
||||||
|
if (diff > 0) {
|
||||||
|
this.forward(tr, diff, true)
|
||||||
|
} else if (diff < 0) {
|
||||||
|
this.backward(tr, -diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using skipUncountables=false within a "useSearchMarker" call, it is recommended
|
||||||
|
* to move the marker to the end. @todo do this after each useSearchMarkerCall
|
||||||
|
*
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {number} len
|
||||||
|
* @param {boolean} skipUncountables Iterate as much as possible iterating over uncountables until we find the next item.
|
||||||
|
*/
|
||||||
|
forward (tr, len, skipUncountables) {
|
||||||
|
if (len === 0 && this.nextItem == null) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
if (this.index + len > this.type._length || this.nextItem == null) {
|
||||||
|
throw lengthExceeded
|
||||||
|
}
|
||||||
|
let item = /** @type {Item} */ (this.nextItem)
|
||||||
|
this.index += len
|
||||||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||||||
|
while ((!this.reachedEnd || this.currMove !== null) && (len > 0 || (skipUncountables && 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
|
||||||
|
popMovedStack(tr, this)
|
||||||
|
} else if (item === null) {
|
||||||
|
error.unexpectedCase() // should never happen
|
||||||
|
} else if (item.countable && !item.deleted && item.moved === this.currMove && len > 0) {
|
||||||
|
len -= item.length
|
||||||
|
if (len < 0) {
|
||||||
|
this.rel = item.length + len
|
||||||
|
len = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
|
||||||
|
if (this.currMove) {
|
||||||
|
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
|
||||||
|
}
|
||||||
|
const { start, end } = getMovedCoords(item.content, tr, false)
|
||||||
|
this.currMove = item
|
||||||
|
this.currMoveStart = start
|
||||||
|
this.currMoveEnd = end
|
||||||
|
item = start
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (this.reachedEnd) {
|
||||||
|
throw error.unexpectedCase
|
||||||
|
}
|
||||||
|
if (item.right) {
|
||||||
|
item = item.right
|
||||||
|
} else {
|
||||||
|
this.reachedEnd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.index -= len
|
||||||
|
this.nextItem = item
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We prefer to insert content outside of a moved range.
|
||||||
|
* Try to escape the moved range by walking to the left over deleted items.
|
||||||
|
*
|
||||||
|
* @param {Transaction} tr
|
||||||
|
*/
|
||||||
|
reduceMoveDepth (tr) {
|
||||||
|
let nextItem = this.nextItem
|
||||||
|
if (nextItem !== null) {
|
||||||
|
while (this.currMove) {
|
||||||
|
if (nextItem === this.currMoveStart) {
|
||||||
|
nextItem = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
|
||||||
|
popMovedStack(tr, this)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// check if we can iterate to the left while stepping over deleted items until we find an item === this.currMoveStart
|
||||||
|
/**
|
||||||
|
* @type {Item} nextItem
|
||||||
|
*/
|
||||||
|
let item = nextItem
|
||||||
|
while (item.deleted && item.moved === this.currMove && item !== this.currMoveStart) {
|
||||||
|
item = /** @type {Item} */ (item.left) // this must exist otherwise we miscalculated the move
|
||||||
|
}
|
||||||
|
if (item === this.currMoveStart) {
|
||||||
|
// we only want to iterate over deleted items if we can escape a move
|
||||||
|
nextItem = item
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextItem = nextItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {number} len
|
||||||
|
* @return {ListCursor}
|
||||||
|
*/
|
||||||
|
backward (tr, len) {
|
||||||
|
if (this.index - len < 0) {
|
||||||
|
throw lengthExceeded
|
||||||
|
}
|
||||||
|
this.index -= len
|
||||||
|
if (this.reachedEnd) {
|
||||||
|
const nextItem = /** @type {Item} */ (this.nextItem)
|
||||||
|
this.rel = nextItem.countable && !nextItem.deleted ? nextItem.length : 0
|
||||||
|
this.reachedEnd = false
|
||||||
|
}
|
||||||
|
if (this.rel >= len) {
|
||||||
|
this.rel -= len
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
let item = this.nextItem
|
||||||
|
if (item && item.content.constructor === ContentMove) {
|
||||||
|
item = item.left
|
||||||
|
} else {
|
||||||
|
len += ((item && item.countable && !item.deleted && item.moved === this.currMove) ? item.length : 0) - this.rel
|
||||||
|
}
|
||||||
|
this.rel = 0
|
||||||
|
while (item && len > 0) {
|
||||||
|
if (item.countable && !item.deleted && item.moved === this.currMove) {
|
||||||
|
len -= item.length
|
||||||
|
if (len < 0) {
|
||||||
|
this.rel = -len
|
||||||
|
len = 0
|
||||||
|
}
|
||||||
|
if (len === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
|
||||||
|
if (this.currMove) {
|
||||||
|
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
|
||||||
|
}
|
||||||
|
const { start, end } = getMovedCoords(item.content, tr, false)
|
||||||
|
this.currMove = item
|
||||||
|
this.currMoveStart = start
|
||||||
|
this.currMoveEnd = end
|
||||||
|
item = start
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (item === this.currMoveStart) {
|
||||||
|
item = /** @type {Item} */ (this.currMove) // we iterate to the left after the current condition
|
||||||
|
popMovedStack(tr, this)
|
||||||
|
}
|
||||||
|
item = item.left
|
||||||
|
}
|
||||||
|
this.nextItem = item
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {{length: number}} T
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {number} len
|
||||||
|
* @param {T} value the initial content
|
||||||
|
* @param {function(AbstractContent, number, number):T} slice
|
||||||
|
* @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 (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 (this.rel + slicedContent.length === nextItem.length) {
|
||||||
|
this.rel = 0
|
||||||
|
} else {
|
||||||
|
this.rel += slicedContent.length
|
||||||
|
continue // do not iterate to item.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextItem.right) {
|
||||||
|
nextItem = nextItem.right
|
||||||
|
} else {
|
||||||
|
this.reachedEnd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((!this.reachedEnd || this.currMove !== null) && len > 0) {
|
||||||
|
// always set nextItem before any method call
|
||||||
|
this.nextItem = nextItem
|
||||||
|
this.forward(tr, 0, true)
|
||||||
|
nextItem = this.nextItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextItem = nextItem
|
||||||
|
if (len < 0) {
|
||||||
|
this.index -= len
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
delete (tr, len) {
|
||||||
|
const startLength = len
|
||||||
|
const sm = this.type._searchMarker
|
||||||
|
let item = this.nextItem
|
||||||
|
if (this.index + len > this.type._length) {
|
||||||
|
throw lengthExceeded
|
||||||
|
}
|
||||||
|
while (len > 0) {
|
||||||
|
while (item && !item.deleted && item.countable && !this.reachedEnd && len > 0 && item.moved === this.currMove && item !== this.currMoveEnd) {
|
||||||
|
if (this.rel > 0) {
|
||||||
|
item = getItemCleanStart(tr, createID(item.id.client, item.id.clock + this.rel))
|
||||||
|
this.rel = 0
|
||||||
|
}
|
||||||
|
if (len < item.length) {
|
||||||
|
getItemCleanStart(tr, createID(item.id.client, item.id.clock + len))
|
||||||
|
}
|
||||||
|
len -= item.length
|
||||||
|
item.delete(tr)
|
||||||
|
if (item.right) {
|
||||||
|
item = item.right
|
||||||
|
} else {
|
||||||
|
this.reachedEnd = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
|
this.nextItem = item
|
||||||
|
this.forward(tr, 0, true)
|
||||||
|
item = this.nextItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextItem = item
|
||||||
|
if (sm) {
|
||||||
|
updateMarkerChanges(sm, this.index, -startLength + len, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
*/
|
||||||
|
_splitRel (tr) {
|
||||||
|
if (this.rel > 0) {
|
||||||
|
/**
|
||||||
|
* @type {ID}
|
||||||
|
*/
|
||||||
|
const itemid = /** @type {Item} */ (this.nextItem).id
|
||||||
|
this.nextItem = getItemCleanStart(tr, createID(itemid.client, itemid.clock + this.rel))
|
||||||
|
this.rel = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Important: you must update markers after calling this method!
|
||||||
|
*
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {Array<AbstractContent>} content
|
||||||
|
*/
|
||||||
|
insertContents (tr, content) {
|
||||||
|
this.reduceMoveDepth(tr)
|
||||||
|
this._splitRel(tr)
|
||||||
|
const parent = this.type
|
||||||
|
const store = tr.doc.store
|
||||||
|
const ownClientId = tr.doc.clientID
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
const right = this.right
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
let left = this.left
|
||||||
|
content.forEach(c => {
|
||||||
|
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, c)
|
||||||
|
left.integrate(tr, 0)
|
||||||
|
})
|
||||||
|
if (right === null) {
|
||||||
|
this.nextItem = left
|
||||||
|
this.reachedEnd = true
|
||||||
|
} else {
|
||||||
|
this.nextItem = right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {Array<{ start: RelativePosition, end: RelativePosition }>} ranges
|
||||||
|
*/
|
||||||
|
insertMove (tr, ranges) {
|
||||||
|
this.insertContents(tr, ranges.map(range => new ContentMove(range.start, range.end, -1)))
|
||||||
|
// @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 // @todo instead, iterate through sm and delete all marked properties on items
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} values
|
||||||
|
*/
|
||||||
|
insertArrayValue (tr, values) {
|
||||||
|
this._splitRel(tr)
|
||||||
|
const sm = this.type._searchMarker
|
||||||
|
/**
|
||||||
|
* @type {Array<AbstractContent>}
|
||||||
|
*/
|
||||||
|
const contents = []
|
||||||
|
/**
|
||||||
|
* @type {Array<Object|Array<any>|number|null>}
|
||||||
|
*/
|
||||||
|
let jsonContent = []
|
||||||
|
const packJsonContent = () => {
|
||||||
|
if (jsonContent.length > 0) {
|
||||||
|
contents.push(new ContentAny(jsonContent))
|
||||||
|
jsonContent = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.forEach(c => {
|
||||||
|
if (c === null) {
|
||||||
|
jsonContent.push(c)
|
||||||
|
} else {
|
||||||
|
switch (c.constructor) {
|
||||||
|
case Number:
|
||||||
|
case Object:
|
||||||
|
case Boolean:
|
||||||
|
case Array:
|
||||||
|
case String:
|
||||||
|
jsonContent.push(c)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
packJsonContent()
|
||||||
|
switch (c.constructor) {
|
||||||
|
case Uint8Array:
|
||||||
|
case ArrayBuffer:
|
||||||
|
contents.push(new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
||||||
|
break
|
||||||
|
case Doc:
|
||||||
|
contents.push(new ContentDoc(/** @type {Doc} */ (c)))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (c instanceof AbstractType) {
|
||||||
|
contents.push(new ContentType(c))
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected content type in insert operation')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
packJsonContent()
|
||||||
|
this.insertContents(tr, contents)
|
||||||
|
this.index += values.length
|
||||||
|
if (sm) {
|
||||||
|
updateMarkerChanges(sm, this.index - values.length, values.length, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
slice (tr, len) {
|
||||||
|
return this._slice(tr, len, [], sliceArrayContent, concatArrayContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {function(any, number, any):void} f
|
||||||
|
*/
|
||||||
|
forEach (tr, f) {
|
||||||
|
for (const val of this.values(tr)) {
|
||||||
|
// decrease index because retrieving value will increase index
|
||||||
|
f(val, this.index - 1, this.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {function(any, number, any):T} f
|
||||||
|
* @return {Array<T>}
|
||||||
|
*/
|
||||||
|
map (tr, f) {
|
||||||
|
const arr = new Array(this.type._length - this.index)
|
||||||
|
let i = 0
|
||||||
|
for (const val of this.values(tr)) {
|
||||||
|
arr[i++] = f(val, this.index - 1, this.type)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
*/
|
||||||
|
values (tr) {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
next: () => {
|
||||||
|
if (this.reachedEnd || this.index === this.type._length) {
|
||||||
|
return { done: true }
|
||||||
|
}
|
||||||
|
const [value] = this.slice(tr, 1)
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractContent} itemcontent
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
const sliceArrayContent = (itemcontent, start, len) => {
|
||||||
|
const content = itemcontent.getContent()
|
||||||
|
return content.length <= len && start === 0 ? content : content.slice(start, start + len)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Array<any>} content
|
||||||
|
* @param {Array<any>} added
|
||||||
|
*/
|
||||||
|
const concatArrayContent = (content, added) => {
|
||||||
|
content.push(...added)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move-ranges must not cross each other.
|
||||||
|
*
|
||||||
|
* This function computes the minimal amount of ranges to move a range of content to
|
||||||
|
* a different place.
|
||||||
|
*
|
||||||
|
* Algorithm:
|
||||||
|
* * Store the current stack in $preStack and $preItem = walker.nextItem
|
||||||
|
* * Iterate forward $len items.
|
||||||
|
* * The current stack is stored is $afterStack and $
|
||||||
|
* * Delete the stack-items that both of them have in common
|
||||||
|
*
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {ListCursor} walker
|
||||||
|
* @param {number} len
|
||||||
|
* @return {Array<{ start: RelativePosition, end: RelativePosition }>}
|
||||||
|
*/
|
||||||
|
export const getMinimalListViewRanges = (tr, walker, len) => {
|
||||||
|
if (len === 0) return []
|
||||||
|
if (walker.index + len > walker.type._length) {
|
||||||
|
throw lengthExceeded
|
||||||
|
}
|
||||||
|
// stepping outside the current move-range as much as possible
|
||||||
|
walker.reduceMoveDepth(tr)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<{ start: RelativePosition, end: RelativePosition, move: Item | null }>}
|
||||||
|
*/
|
||||||
|
const ranges = []
|
||||||
|
// store relevant information for the beginning, before we iterate forward
|
||||||
|
/**
|
||||||
|
* @type {Array<Item>}
|
||||||
|
*/
|
||||||
|
const preStack = walker.movedStack.map(si => si.move)
|
||||||
|
const preMove = walker.currMove
|
||||||
|
const preItem = /** @type {Item} */ (walker.nextItem)
|
||||||
|
const preRel = walker.rel
|
||||||
|
|
||||||
|
walker.forward(tr, len, false)
|
||||||
|
|
||||||
|
// store the same information for the end, after we iterate forward
|
||||||
|
/**
|
||||||
|
* @type {Array<Item>}
|
||||||
|
*/
|
||||||
|
const afterStack = walker.movedStack.map(si => si.move)
|
||||||
|
const afterMove = walker.currMove
|
||||||
|
/**
|
||||||
|
const nextIsCurrMoveStart = walker.nextItem === walker.currMoveStart
|
||||||
|
const afterItem = /** @type {Item} / (nextIsCurrMoveStart
|
||||||
|
? walker.currMove
|
||||||
|
: (walker.rel > 0 || walker.reachedEnd)
|
||||||
|
? walker.nextItem
|
||||||
|
: /** @type {Item} / (walker.nextItem).left
|
||||||
|
) */
|
||||||
|
const afterItem = /** @type {Item} */ (
|
||||||
|
(walker.rel > 0 || walker.reachedEnd)
|
||||||
|
? walker.nextItem
|
||||||
|
: /** @type {Item} */ (walker.nextItem).left
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* afterRel is always > 0
|
||||||
|
*/
|
||||||
|
const afterRel = walker.rel > 0
|
||||||
|
? walker.rel
|
||||||
|
: afterItem.length
|
||||||
|
|
||||||
|
walker.forward(tr, 0, false) // @todo remove once this is done is useSearchMarker
|
||||||
|
|
||||||
|
let start = createRelativePosition(walker.type, createID(preItem.id.client, preItem.id.clock + preRel), 0)
|
||||||
|
let end = createRelativePosition(
|
||||||
|
walker.type,
|
||||||
|
createID(afterItem.id.client, afterItem.id.clock + afterRel - 1),
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
|
||||||
|
if (preMove) {
|
||||||
|
preStack.push(preMove)
|
||||||
|
}
|
||||||
|
if (afterMove) {
|
||||||
|
afterStack.push(afterMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove common stack-items
|
||||||
|
while (preStack.length > 0 && preStack[0] === afterStack[0]) {
|
||||||
|
preStack.shift()
|
||||||
|
afterStack.shift()
|
||||||
|
}
|
||||||
|
const topLevelMove = preStack.length > 0 ? preStack[0].moved : (afterStack.length > 0 ? afterStack[0].moved : null)
|
||||||
|
|
||||||
|
// remove stack-items that are useless for our computation (that wouldn't produce meaningful ranges)
|
||||||
|
// @todo
|
||||||
|
|
||||||
|
while (preStack.length > 0) {
|
||||||
|
const move = /** @type {Item} */ (preStack.pop())
|
||||||
|
ranges.push({
|
||||||
|
start,
|
||||||
|
end: /** @type {ContentMove} */ (move.content).end,
|
||||||
|
move
|
||||||
|
})
|
||||||
|
start = createRelativePosition(walker.type, createID(move.id.client, move.id.clock), -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const middleMove = { start, end, move: topLevelMove }
|
||||||
|
ranges.push(middleMove)
|
||||||
|
|
||||||
|
while (afterStack.length > 0) {
|
||||||
|
const move = /** @type {Item} */ (afterStack.pop())
|
||||||
|
ranges.push({
|
||||||
|
start: /** @type {ContentMove} */ (move.content).start,
|
||||||
|
end,
|
||||||
|
move
|
||||||
|
})
|
||||||
|
end = createRelativePosition(walker.type, createID(move.id.client, move.id.clock), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update end of the center move operation
|
||||||
|
// Move ranges must be applied in order
|
||||||
|
middleMove.end = end
|
||||||
|
|
||||||
|
const normalizedRanges = array.flatten(ranges.map(range => {
|
||||||
|
// A subset of a range could be moved by another move with a higher priority.
|
||||||
|
// If that is the case, we need to ignore those moved items.
|
||||||
|
const { start, end } = getMovedCoords(range, tr, false)
|
||||||
|
const move = range.move
|
||||||
|
const ranges = []
|
||||||
|
/**
|
||||||
|
* @type {RelativePosition | null}
|
||||||
|
*/
|
||||||
|
let rangeStart = range.start
|
||||||
|
/**
|
||||||
|
* @type {Item}
|
||||||
|
*/
|
||||||
|
let item = start
|
||||||
|
while (item !== end) {
|
||||||
|
if (item.moved !== move && rangeStart != null) {
|
||||||
|
ranges.push({ start: rangeStart, end: createRelativePosition(walker.type, createID(item.id.client, item.id.clock), 0) })
|
||||||
|
rangeStart = null
|
||||||
|
}
|
||||||
|
if (item.moved === move && rangeStart === null) {
|
||||||
|
// @todo It might be better to set this to item.left, with assoc -1
|
||||||
|
rangeStart = createRelativePosition(walker.type, createID(item.id.client, item.id.clock), 0)
|
||||||
|
}
|
||||||
|
item = /** @type {Item} */ (item.right)
|
||||||
|
}
|
||||||
|
if (rangeStart != null) {
|
||||||
|
ranges.push({
|
||||||
|
start: rangeStart,
|
||||||
|
end: range.end
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ranges
|
||||||
|
}))
|
||||||
|
|
||||||
|
// filter out unnecessary ranges
|
||||||
|
return normalizedRanges.filter(range => !compareRelativePositions(range.start, range.end))
|
||||||
|
}
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
import * as error from 'lib0/error'
|
|
||||||
|
|
||||||
import {
|
|
||||||
getItemCleanStart,
|
|
||||||
createID,
|
|
||||||
getMovedCoords,
|
|
||||||
updateMarkerChanges,
|
|
||||||
getState,
|
|
||||||
ContentAny,
|
|
||||||
ContentBinary,
|
|
||||||
ContentType,
|
|
||||||
ContentDoc,
|
|
||||||
Doc,
|
|
||||||
RelativePosition, ID, AbstractContent, ContentMove, Transaction, Item, AbstractType // eslint-disable-line
|
|
||||||
} from '../internals.js'
|
|
||||||
|
|
||||||
const lengthExceeded = error.create('Length exceeded!')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @todo rename to walker?
|
|
||||||
* @todo check that inserting character one after another always reuses ListIterators
|
|
||||||
*/
|
|
||||||
export class ListIterator {
|
|
||||||
/**
|
|
||||||
* @param {AbstractType<any>} type
|
|
||||||
*/
|
|
||||||
constructor (type) {
|
|
||||||
this.type = type
|
|
||||||
/**
|
|
||||||
* Current index-position
|
|
||||||
*/
|
|
||||||
this.index = 0
|
|
||||||
/**
|
|
||||||
* Relative position to the current item (if item.content.length > 1)
|
|
||||||
*/
|
|
||||||
this.rel = 0
|
|
||||||
/**
|
|
||||||
* This refers to the current right item, unless reachedEnd is true. Then it refers to the left item.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this.nextItem = type._start
|
|
||||||
this.reachedEnd = type._start === null
|
|
||||||
/**
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this.currMove = null
|
|
||||||
/**
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this.currMoveStart = null
|
|
||||||
/**
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
this.currMoveEnd = null
|
|
||||||
/**
|
|
||||||
* @type {Array<{ start: Item | null, end: Item | null, move: Item }>}
|
|
||||||
*/
|
|
||||||
this.movedStack = []
|
|
||||||
}
|
|
||||||
|
|
||||||
clone () {
|
|
||||||
const iter = new ListIterator(this.type)
|
|
||||||
iter.index = this.index
|
|
||||||
iter.rel = this.rel
|
|
||||||
iter.nextItem = this.nextItem
|
|
||||||
iter.reachedEnd = this.reachedEnd
|
|
||||||
iter.currMove = this.currMove
|
|
||||||
iter.currMoveStart = this.currMoveStart
|
|
||||||
iter.currMoveEnd = this.currMoveEnd
|
|
||||||
iter.movedStack = this.movedStack.slice()
|
|
||||||
return iter
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
get left () {
|
|
||||||
if (this.reachedEnd) {
|
|
||||||
return this.nextItem
|
|
||||||
} else {
|
|
||||||
return this.nextItem && this.nextItem.left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
get right () {
|
|
||||||
if (this.reachedEnd) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
return this.nextItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {number} index
|
|
||||||
*/
|
|
||||||
moveTo (tr, index) {
|
|
||||||
const diff = index - this.index
|
|
||||||
if (diff > 0) {
|
|
||||||
this.forward(tr, diff)
|
|
||||||
} else if (diff < 0) {
|
|
||||||
this.backward(tr, -diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {number} len
|
|
||||||
*/
|
|
||||||
forward (tr, len) {
|
|
||||||
if (this.index + len > this.type._length) {
|
|
||||||
throw lengthExceeded
|
|
||||||
}
|
|
||||||
let item = this.nextItem
|
|
||||||
this.index += len
|
|
||||||
if (this.rel) {
|
|
||||||
len += this.rel
|
|
||||||
this.rel = 0
|
|
||||||
}
|
|
||||||
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
|
|
||||||
} else if (item === null) {
|
|
||||||
break
|
|
||||||
} else if (item.countable && !item.deleted && item.moved === this.currMove && len > 0) {
|
|
||||||
len -= item.length
|
|
||||||
if (len < 0) {
|
|
||||||
this.rel = item.length + len
|
|
||||||
len = 0
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
|
|
||||||
if (this.currMove) {
|
|
||||||
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
|
|
||||||
}
|
|
||||||
const { start, end } = getMovedCoords(item.content, tr)
|
|
||||||
this.currMove = item
|
|
||||||
this.currMoveStart = start
|
|
||||||
this.currMoveEnd = end
|
|
||||||
item = start
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (item.right) {
|
|
||||||
item = item.right
|
|
||||||
} else {
|
|
||||||
this.reachedEnd = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.index -= len
|
|
||||||
this.nextItem = item
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
*/
|
|
||||||
reduceMoves (tr) {
|
|
||||||
let item = this.nextItem
|
|
||||||
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
|
|
||||||
}
|
|
||||||
this.nextItem = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {number} len
|
|
||||||
* @return {ListIterator}
|
|
||||||
*/
|
|
||||||
backward (tr, len) {
|
|
||||||
if (this.index - len < 0) {
|
|
||||||
throw lengthExceeded
|
|
||||||
}
|
|
||||||
this.index -= len
|
|
||||||
if (this.reachedEnd) {
|
|
||||||
const nextItem = /** @type {Item} */ (this.nextItem)
|
|
||||||
this.rel = nextItem.countable && !nextItem.deleted ? nextItem.length : 0
|
|
||||||
this.reachedEnd = false
|
|
||||||
}
|
|
||||||
if (this.rel >= len) {
|
|
||||||
this.rel -= len
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
let item = this.nextItem && this.nextItem.left
|
|
||||||
if (this.rel) {
|
|
||||||
len -= this.rel
|
|
||||||
this.rel = 0
|
|
||||||
}
|
|
||||||
while (item && len > 0) {
|
|
||||||
if (item.countable && !item.deleted && item.moved === this.currMove) {
|
|
||||||
len -= item.length
|
|
||||||
if (len < 0) {
|
|
||||||
this.rel = -len
|
|
||||||
len = 0
|
|
||||||
}
|
|
||||||
if (len === 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if (item.content.constructor === ContentMove && item.moved === this.currMove) {
|
|
||||||
if (this.currMove) {
|
|
||||||
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
|
|
||||||
}
|
|
||||||
const { start, end } = getMovedCoords(item.content, tr)
|
|
||||||
this.currMove = item
|
|
||||||
this.currMoveStart = start
|
|
||||||
this.currMoveEnd = end
|
|
||||||
item = start
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
item = item.left
|
|
||||||
}
|
|
||||||
this.nextItem = item
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template {{length: number}} T
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {number} len
|
|
||||||
* @param {T} value the initial content
|
|
||||||
* @param {function(AbstractContent, number, number):T} slice
|
|
||||||
* @param {function(T, T): T} concat
|
|
||||||
*/
|
|
||||||
_slice (tr, len, value, slice, concat) {
|
|
||||||
this.index += len
|
|
||||||
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)
|
|
||||||
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.nextItem.right) {
|
|
||||||
this.nextItem = this.nextItem.right
|
|
||||||
} else {
|
|
||||||
this.reachedEnd = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.nextItem && (!this.reachedEnd || this.currMove !== null) && len > 0) {
|
|
||||||
this.forward(tr, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (len < 0) {
|
|
||||||
this.index -= len
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {number} len
|
|
||||||
*/
|
|
||||||
delete (tr, len) {
|
|
||||||
const startLength = len
|
|
||||||
const sm = this.type._searchMarker
|
|
||||||
let item = this.nextItem
|
|
||||||
while (len > 0 && !this.reachedEnd) {
|
|
||||||
while (item && !item.deleted && item.countable && !this.reachedEnd && len > 0) {
|
|
||||||
if (this.rel > 0) {
|
|
||||||
item = getItemCleanStart(tr, createID(item.id.client, item.id.clock + this.rel))
|
|
||||||
this.rel = 0
|
|
||||||
}
|
|
||||||
if (len < item.length) {
|
|
||||||
getItemCleanStart(tr, createID(item.id.client, item.id.clock + len))
|
|
||||||
}
|
|
||||||
len -= item.length
|
|
||||||
item.delete(tr)
|
|
||||||
if (item.right) {
|
|
||||||
item = item.right
|
|
||||||
} else {
|
|
||||||
this.reachedEnd = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item && !this.reachedEnd && len > 0) {
|
|
||||||
this.nextItem = item
|
|
||||||
this.forward(tr, 0)
|
|
||||||
item = this.nextItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.nextItem = item
|
|
||||||
if (sm) {
|
|
||||||
updateMarkerChanges(sm, this.index, -startLength + len, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
*/
|
|
||||||
_splitRel (tr) {
|
|
||||||
if (this.rel > 0) {
|
|
||||||
/**
|
|
||||||
* @type {ID}
|
|
||||||
*/
|
|
||||||
const itemid = /** @type {Item} */ (this.nextItem).id
|
|
||||||
this.nextItem = getItemCleanStart(tr, createID(itemid.client, itemid.clock + this.rel))
|
|
||||||
this.rel = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Important: you must update markers after calling this method!
|
|
||||||
*
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {Array<AbstractContent>} content
|
|
||||||
*/
|
|
||||||
insertContents (tr, content) {
|
|
||||||
this.reduceMoves(tr)
|
|
||||||
this._splitRel(tr)
|
|
||||||
const parent = this.type
|
|
||||||
const store = tr.doc.store
|
|
||||||
const ownClientId = tr.doc.clientID
|
|
||||||
/**
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
const right = this.right
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Item | null}
|
|
||||||
*/
|
|
||||||
let left = this.left
|
|
||||||
content.forEach(c => {
|
|
||||||
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, c)
|
|
||||||
left.integrate(tr, 0)
|
|
||||||
})
|
|
||||||
if (right === null) {
|
|
||||||
this.nextItem = left
|
|
||||||
this.reachedEnd = true
|
|
||||||
} else {
|
|
||||||
this.nextItem = right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {RelativePosition} start
|
|
||||||
* @param {RelativePosition} end
|
|
||||||
*/
|
|
||||||
insertMove (tr, start, end) {
|
|
||||||
this.insertContents(tr, [new ContentMove(start, end, -1)]) // @todo adjust priority
|
|
||||||
// @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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} values
|
|
||||||
*/
|
|
||||||
insertArrayValue (tr, values) {
|
|
||||||
this._splitRel(tr)
|
|
||||||
const sm = this.type._searchMarker
|
|
||||||
/**
|
|
||||||
* @type {Array<AbstractContent>}
|
|
||||||
*/
|
|
||||||
const contents = []
|
|
||||||
/**
|
|
||||||
* @type {Array<Object|Array<any>|number|null>}
|
|
||||||
*/
|
|
||||||
let jsonContent = []
|
|
||||||
const packJsonContent = () => {
|
|
||||||
if (jsonContent.length > 0) {
|
|
||||||
contents.push(new ContentAny(jsonContent))
|
|
||||||
jsonContent = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
values.forEach(c => {
|
|
||||||
if (c === null) {
|
|
||||||
jsonContent.push(c)
|
|
||||||
} else {
|
|
||||||
switch (c.constructor) {
|
|
||||||
case Number:
|
|
||||||
case Object:
|
|
||||||
case Boolean:
|
|
||||||
case Array:
|
|
||||||
case String:
|
|
||||||
jsonContent.push(c)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
packJsonContent()
|
|
||||||
switch (c.constructor) {
|
|
||||||
case Uint8Array:
|
|
||||||
case ArrayBuffer:
|
|
||||||
contents.push(new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
|
||||||
break
|
|
||||||
case Doc:
|
|
||||||
contents.push(new ContentDoc(/** @type {Doc} */ (c)))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
if (c instanceof AbstractType) {
|
|
||||||
contents.push(new ContentType(c))
|
|
||||||
} else {
|
|
||||||
throw new Error('Unexpected content type in insert operation')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
packJsonContent()
|
|
||||||
this.insertContents(tr, contents)
|
|
||||||
this.index += values.length
|
|
||||||
if (sm) {
|
|
||||||
updateMarkerChanges(sm, this.index - values.length, values.length, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {number} len
|
|
||||||
*/
|
|
||||||
slice (tr, len) {
|
|
||||||
return this._slice(tr, len, [], sliceArrayContent, concatArrayContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {function(any, number, any):void} f
|
|
||||||
*/
|
|
||||||
forEach (tr, f) {
|
|
||||||
for (const val of this.values(tr)) {
|
|
||||||
f(val, this.index, this.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @template T
|
|
||||||
* @param {Transaction} tr
|
|
||||||
* @param {function(any, number, any):T} f
|
|
||||||
* @return {Array<T>}
|
|
||||||
*/
|
|
||||||
map (tr, f) {
|
|
||||||
const arr = new Array(this.type._length - this.index)
|
|
||||||
let i = 0
|
|
||||||
for (const val of this.values(tr)) {
|
|
||||||
arr[i++] = f(val, this.index, this.type)
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tr
|
|
||||||
*/
|
|
||||||
values (tr) {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator] () {
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
next: () => {
|
|
||||||
if (this.reachedEnd || this.index === this.type._length) {
|
|
||||||
return { done: true }
|
|
||||||
}
|
|
||||||
const [value] = this.slice(tr, 1)
|
|
||||||
return {
|
|
||||||
done: false,
|
|
||||||
value: value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {AbstractContent} itemcontent
|
|
||||||
* @param {number} start
|
|
||||||
* @param {number} len
|
|
||||||
*/
|
|
||||||
const sliceArrayContent = (itemcontent, start, len) => {
|
|
||||||
const content = itemcontent.getContent()
|
|
||||||
return content.length <= len && start === 0 ? content : content.slice(start, start + len)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param {Array<any>} content
|
|
||||||
* @param {Array<any>} added
|
|
||||||
*/
|
|
||||||
const concatArrayContent = (content, added) => {
|
|
||||||
content.push(...added)
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
@@ -196,7 +196,7 @@ export const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => {
|
|||||||
export const writeRelativePosition = (encoder, rpos) => {
|
export const writeRelativePosition = (encoder, rpos) => {
|
||||||
const { type, tname, item, assoc } = rpos
|
const { type, tname, item, assoc } = rpos
|
||||||
if (item !== null) {
|
if (item !== null) {
|
||||||
encoding.writeVarUint(encoder, 0)
|
encoding.writeUint8(encoder, 0)
|
||||||
writeID(encoder, item)
|
writeID(encoder, item)
|
||||||
} else if (tname !== null) {
|
} else if (tname !== null) {
|
||||||
// case 2: found position at the end of the list and type is stored in y.share
|
// case 2: found position at the end of the list and type is stored in y.share
|
||||||
@@ -233,7 +233,7 @@ export const readRelativePosition = decoder => {
|
|||||||
let type = null
|
let type = null
|
||||||
let tname = null
|
let tname = null
|
||||||
let itemID = null
|
let itemID = null
|
||||||
switch (decoding.readVarUint(decoder)) {
|
switch (decoding.readUint8(decoder)) {
|
||||||
case 0:
|
case 0:
|
||||||
// case 1: found position somewhere in the linked list
|
// case 1: found position somewhere in the linked list
|
||||||
itemID = readID(decoder)
|
itemID = readID(decoder)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding'
|
||||||
import * as map from 'lib0/map'
|
import * as map from 'lib0/map'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
import * as set from 'lib0/set'
|
import * as set from 'lib0/set'
|
||||||
@@ -61,6 +62,13 @@ export class Transaction {
|
|||||||
* @type {DeleteSet}
|
* @type {DeleteSet}
|
||||||
*/
|
*/
|
||||||
this.deleteSet = new DeleteSet()
|
this.deleteSet = new DeleteSet()
|
||||||
|
/**
|
||||||
|
* These deletes were used to cleanup the document and
|
||||||
|
* should be broadcasted again using a different transaction-origin.
|
||||||
|
*
|
||||||
|
* @type {DeleteSet}
|
||||||
|
*/
|
||||||
|
this.cleanupDeletions = new DeleteSet()
|
||||||
/**
|
/**
|
||||||
* Holds the state before the transaction started.
|
* Holds the state before the transaction started.
|
||||||
* @type {Map<Number,Number>}
|
* @type {Map<Number,Number>}
|
||||||
@@ -140,6 +148,18 @@ export const writeUpdateMessageFromTransaction = (encoder, transaction) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const writeCleanupMessageFromTransaction = (encoder, transaction) => {
|
||||||
|
const ds = transaction.cleanupDeletions
|
||||||
|
sortAndMergeDeleteSet(ds)
|
||||||
|
// write structs: 0 structs were created
|
||||||
|
encoding.writeVarUint(encoder.restEncoder, 0)
|
||||||
|
writeDeleteSet(encoder, ds)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
*
|
*
|
||||||
@@ -344,11 +364,17 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
}
|
}
|
||||||
// @todo Merge all the transactions into one and provide send the data as a single update message
|
// @todo Merge all the transactions into one and provide send the data as a single update message
|
||||||
doc.emit('afterTransactionCleanup', [transaction, doc])
|
doc.emit('afterTransactionCleanup', [transaction, doc])
|
||||||
|
const needsCleanupEvent = transaction.cleanupDeletions.clients.size > 0
|
||||||
if (doc._observers.has('update')) {
|
if (doc._observers.has('update')) {
|
||||||
const encoder = new UpdateEncoderV1()
|
const encoder = new UpdateEncoderV1()
|
||||||
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
||||||
if (hasContent) {
|
if (hasContent) {
|
||||||
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction])
|
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction])
|
||||||
|
if (needsCleanupEvent) {
|
||||||
|
const encoder = new UpdateEncoderV1()
|
||||||
|
writeCleanupMessageFromTransaction(encoder, transaction)
|
||||||
|
doc.emit('update', [encoder.toUint8Array(), 'cleanup', doc, transaction])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (doc._observers.has('updateV2')) {
|
if (doc._observers.has('updateV2')) {
|
||||||
@@ -356,6 +382,11 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction)
|
||||||
if (hasContent) {
|
if (hasContent) {
|
||||||
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
|
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction])
|
||||||
|
if (needsCleanupEvent) {
|
||||||
|
const encoder = new UpdateEncoderV2()
|
||||||
|
writeCleanupMessageFromTransaction(encoder, transaction)
|
||||||
|
doc.emit('updateV2', [encoder.toUint8Array(), 'cleanup', doc, transaction])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
|
const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction
|
||||||
@@ -424,3 +455,10 @@ export const transact = (doc, f, origin = null, local = true) => {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} tr
|
||||||
|
* @param {AbstractStruct} struct
|
||||||
|
*/
|
||||||
|
export const addsStruct = (tr, struct) =>
|
||||||
|
struct.id.clock >= (tr.beforeState.get(struct.id.client) || 0)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
|
|
||||||
import * as set from 'lib0/set'
|
import * as set from 'lib0/set'
|
||||||
import * as array from 'lib0/array'
|
import * as array from 'lib0/array'
|
||||||
|
import { addsStruct } from './Transaction.js'
|
||||||
|
import { ListCursor } from './ListCursor.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* YEvent describes the changes on a YType.
|
* YEvent describes the changes on a YType.
|
||||||
@@ -61,7 +63,7 @@ export class YEvent {
|
|||||||
*/
|
*/
|
||||||
get path () {
|
get path () {
|
||||||
// @ts-ignore _item is defined because target is integrated
|
// @ts-ignore _item is defined because target is integrated
|
||||||
return getPathTo(this.currentTarget, this.target)
|
return getPathTo(this.currentTarget, this.target, this.transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,11 +143,13 @@ export class YEvent {
|
|||||||
*
|
*
|
||||||
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
|
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
|
||||||
*
|
*
|
||||||
|
* @todo this can be removed in the next release (prefer function)
|
||||||
|
*
|
||||||
* @param {AbstractStruct} struct
|
* @param {AbstractStruct} struct
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
adds (struct) {
|
adds (struct) {
|
||||||
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
|
return addsStruct(this.transaction, struct)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,7 +175,7 @@ export class YEvent {
|
|||||||
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
||||||
if (changed.has(null)) {
|
if (changed.has(null)) {
|
||||||
/**
|
/**
|
||||||
* @type {Array<{ end: Item | null, move: Item | null, isNew : boolean }>}
|
* @type {Array<{ end: Item | null, move: Item | null, isNew: boolean, isDeleted: boolean }>}
|
||||||
*/
|
*/
|
||||||
const movedStack = []
|
const movedStack = []
|
||||||
/**
|
/**
|
||||||
@@ -182,6 +186,10 @@ export class YEvent {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
let currMoveIsNew = false
|
let currMoveIsNew = false
|
||||||
|
/**
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
let currMoveIsDeleted = false
|
||||||
/**
|
/**
|
||||||
* @type {Item | null}
|
* @type {Item | null}
|
||||||
*/
|
*/
|
||||||
@@ -195,25 +203,42 @@ export class YEvent {
|
|||||||
delta.push(lastOp)
|
delta.push(lastOp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let item = target._start; item !== null;) {
|
/**
|
||||||
if (item === currMoveEnd) {
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
const isMovedByNew = item => {
|
||||||
|
let moved = item.moved
|
||||||
|
while (moved != null) {
|
||||||
|
if (this.adds(moved)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
moved = moved.moved
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let item = target._start; ;) {
|
||||||
|
if (item === currMoveEnd && currMove) {
|
||||||
item = currMove
|
item = currMove
|
||||||
const { end, move, isNew } = movedStack.pop() || { end: null, move: null, isNew: false }
|
const { end, move, isNew, isDeleted } = movedStack.pop() || { end: null, move: null, isNew: false, isDeleted: false }
|
||||||
currMoveIsNew = isNew
|
currMoveIsNew = isNew
|
||||||
|
currMoveIsDeleted = isDeleted
|
||||||
currMoveEnd = end
|
currMoveEnd = end
|
||||||
currMove = move
|
currMove = move
|
||||||
|
} else if (item === null) {
|
||||||
|
break
|
||||||
} else if (item.content.constructor === ContentMove) {
|
} else if (item.content.constructor === ContentMove) {
|
||||||
if (item.moved === currMove) {
|
if (item.moved === currMove && (!item.deleted || (this.deletes(item) && !this.adds(item)))) {
|
||||||
movedStack.push({ end: currMoveEnd, move: currMove, isNew: currMoveIsNew })
|
movedStack.push({ end: currMoveEnd, move: currMove, isNew: currMoveIsNew, isDeleted: currMoveIsDeleted })
|
||||||
const { start, end } = getMovedCoords(item.content, tr)
|
const { start, end } = getMovedCoords(item.content, tr, true) // We must split items for move-ranges, for single moves no splitting suffices
|
||||||
currMove = item
|
currMove = item
|
||||||
currMoveEnd = end
|
currMoveEnd = end
|
||||||
currMoveIsNew = this.adds(item)
|
currMoveIsNew = this.adds(item) || currMoveIsNew
|
||||||
|
currMoveIsDeleted = item.deleted || currMoveIsDeleted
|
||||||
item = start
|
item = start
|
||||||
continue // do not move to item.right
|
continue // do not move to item.right
|
||||||
}
|
}
|
||||||
} else if (item.moved !== currMove) {
|
} 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)) && !this.adds(item) && (item.moved === null || isMovedByNew(item) || currMoveIsDeleted) && (this.transaction.prevMoved.get(item) || null) === currMove) {
|
||||||
if (lastOp === null || lastOp.delete === undefined) {
|
if (lastOp === null || lastOp.delete === undefined) {
|
||||||
packOp()
|
packOp()
|
||||||
lastOp = { delete: 0 }
|
lastOp = { delete: 0 }
|
||||||
@@ -221,7 +246,7 @@ export class YEvent {
|
|||||||
lastOp.delete += item.length
|
lastOp.delete += item.length
|
||||||
}
|
}
|
||||||
} else if (item.deleted) {
|
} else if (item.deleted) {
|
||||||
if (!currMoveIsNew && this.deletes(item) && !this.adds(item)) {
|
if (!currMoveIsNew && this.deletes(item) && !this.adds(item) && !this.transaction.prevMoved.has(item)) {
|
||||||
if (lastOp === null || lastOp.delete === undefined) {
|
if (lastOp === null || lastOp.delete === undefined) {
|
||||||
packOp()
|
packOp()
|
||||||
lastOp = { delete: 0 }
|
lastOp = { delete: 0 }
|
||||||
@@ -230,13 +255,16 @@ export class YEvent {
|
|||||||
deleted.add(item)
|
deleted.add(item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currMoveIsNew || this.adds(item)) {
|
if (currMoveIsNew || this.adds(item) || this.transaction.prevMoved.has(item)) {
|
||||||
if (lastOp === null || lastOp.insert === undefined) {
|
if (lastOp === null || lastOp.insert === undefined) {
|
||||||
packOp()
|
packOp()
|
||||||
lastOp = { insert: [] }
|
lastOp = { insert: [] }
|
||||||
}
|
}
|
||||||
|
// @todo push items instead (or splice..)
|
||||||
lastOp.insert = lastOp.insert.concat(item.content.getContent())
|
lastOp.insert = lastOp.insert.concat(item.content.getContent())
|
||||||
added.add(item)
|
if (!currMoveIsNew) {
|
||||||
|
added.add(item)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (lastOp === null || lastOp.retain === undefined) {
|
if (lastOp === null || lastOp.retain === undefined) {
|
||||||
packOp()
|
packOp()
|
||||||
@@ -247,7 +275,7 @@ export class YEvent {
|
|||||||
}
|
}
|
||||||
item = /** @type {Item} */ (item).right
|
item = /** @type {Item} */ (item).right
|
||||||
}
|
}
|
||||||
if (lastOp !== null && lastOp.retain === undefined) {
|
if (lastOp !== null && lastOp.retain == null) {
|
||||||
packOp()
|
packOp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,12 +298,13 @@ export class YEvent {
|
|||||||
*
|
*
|
||||||
* @param {AbstractType<any>} parent
|
* @param {AbstractType<any>} parent
|
||||||
* @param {AbstractType<any>} child target
|
* @param {AbstractType<any>} child target
|
||||||
|
* @param {Transaction} tr
|
||||||
* @return {Array<string|number>} Path to the target
|
* @return {Array<string|number>} Path to the target
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
const getPathTo = (parent, child) => {
|
const getPathTo = (parent, child, tr) => {
|
||||||
const path = []
|
const path = []
|
||||||
while (child._item !== null && child !== parent) {
|
while (child._item !== null && child !== parent) {
|
||||||
if (child._item.parentSub !== null) {
|
if (child._item.parentSub !== null) {
|
||||||
@@ -283,15 +312,11 @@ const getPathTo = (parent, child) => {
|
|||||||
path.unshift(child._item.parentSub)
|
path.unshift(child._item.parentSub)
|
||||||
} else {
|
} else {
|
||||||
// parent is array-ish
|
// parent is array-ish
|
||||||
let i = 0
|
const c = new ListCursor(/** @type {AbstractType<any>} */ (child._item.parent))
|
||||||
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
|
while (c.nextItem != null && !c.reachedEnd && c.nextItem !== child._item) {
|
||||||
while (c !== child._item && c !== null) {
|
c.forward(tr, (c.nextItem.countable && !c.nextItem.deleted) ? c.nextItem.length : 0, true)
|
||||||
if (!c.deleted) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
c = c.right
|
|
||||||
}
|
}
|
||||||
path.unshift(i)
|
path.unshift(c.index)
|
||||||
}
|
}
|
||||||
child = /** @type {AbstractType<any>} */ (child._item.parent)
|
child = /** @type {AbstractType<any>} */ (child._item.parent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ export const readClientsStructRefs = (decoder, doc) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log('time to read: ', performance.now() - start) // @todo remove
|
|
||||||
}
|
}
|
||||||
return clientRefs
|
return clientRefs
|
||||||
}
|
}
|
||||||
@@ -389,10 +388,6 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
|||||||
const store = doc.store
|
const store = doc.store
|
||||||
// let start = performance.now()
|
// let start = performance.now()
|
||||||
const ss = readClientsStructRefs(structDecoder, doc)
|
const ss = readClientsStructRefs(structDecoder, doc)
|
||||||
// console.log('time to read structs: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
// console.log('time to merge: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
const restStructs = integrateStructs(transaction, store, ss)
|
const restStructs = integrateStructs(transaction, store, ss)
|
||||||
const pending = store.pendingStructs
|
const pending = store.pendingStructs
|
||||||
if (pending) {
|
if (pending) {
|
||||||
@@ -416,8 +411,6 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
|||||||
} else {
|
} else {
|
||||||
store.pendingStructs = restStructs
|
store.pendingStructs = restStructs
|
||||||
}
|
}
|
||||||
// console.log('time to integrate: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store)
|
const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store)
|
||||||
if (store.pendingDs) {
|
if (store.pendingDs) {
|
||||||
// @todo we could make a lower-bound state-vector check as we do above
|
// @todo we could make a lower-bound state-vector check as we do above
|
||||||
@@ -437,11 +430,6 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
|||||||
// Either dsRest == null && pendingDs == null OR dsRest != null
|
// Either dsRest == null && pendingDs == null OR dsRest != null
|
||||||
store.pendingDs = dsRest
|
store.pendingDs = dsRest
|
||||||
}
|
}
|
||||||
// console.log('time to cleanup: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
|
|
||||||
// console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
|
|
||||||
// start = performance.now()
|
|
||||||
if (retry) {
|
if (retry) {
|
||||||
const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update
|
const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update
|
||||||
store.pendingStructs = null
|
store.pendingStructs = null
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const testDiffStateVectorOfUpdateIsEmpty = tc => {
|
|||||||
/**
|
/**
|
||||||
* @type {null | Uint8Array}
|
* @type {null | Uint8Array}
|
||||||
*/
|
*/
|
||||||
let sv = /* any */ (null)
|
let sv = /** @type {any} */ (null)
|
||||||
ydoc.getText().insert(0, 'a')
|
ydoc.getText().insert(0, 'a')
|
||||||
ydoc.on('update', update => {
|
ydoc.on('update', update => {
|
||||||
sv = Y.encodeStateVectorFromUpdate(update)
|
sv = Y.encodeStateVectorFromUpdate(update)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as doc from './doc.tests.js'
|
|||||||
import * as snapshot from './snapshot.tests.js'
|
import * as snapshot from './snapshot.tests.js'
|
||||||
import * as updates from './updates.tests.js'
|
import * as updates from './updates.tests.js'
|
||||||
import * as relativePositions from './relativePositions.tests.js'
|
import * as relativePositions from './relativePositions.tests.js'
|
||||||
|
import * as Y from './testHelper.js'
|
||||||
|
|
||||||
import { runTests } from 'lib0/testing'
|
import { runTests } from 'lib0/testing'
|
||||||
import { isBrowser, isNode } from 'lib0/environment'
|
import { isBrowser, isNode } from 'lib0/environment'
|
||||||
@@ -17,6 +18,8 @@ import * as log from 'lib0/logging'
|
|||||||
|
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
log.createVConsole(document.body)
|
log.createVConsole(document.body)
|
||||||
|
// @ts-ignore
|
||||||
|
window.Y = Y
|
||||||
}
|
}
|
||||||
runTests({
|
runTests({
|
||||||
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
|
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
|
||||||
|
|||||||
@@ -373,33 +373,6 @@ export const compare = users => {
|
|||||||
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
t.compare(Y.encodeStateVector(users[i]), Y.encodeStateVector(users[i + 1]))
|
||||||
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
|
||||||
compareStructStores(users[i].store, users[i + 1].store)
|
compareStructStores(users[i].store, users[i + 1].store)
|
||||||
// @todo
|
|
||||||
// test list-iterator
|
|
||||||
// console.log('dutiraneduiaentdr', users[0].getArray('array')._searchMarker)
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
const user = users[0]
|
|
||||||
user.transact(tr => {
|
|
||||||
const type = user.getArray('array')
|
|
||||||
Y.useSearchMarker(tr, type, type.length, walker => {
|
|
||||||
for (let i = type.length; i >= 0; i--) {
|
|
||||||
const otherWalker = new Y.ListIterator(type)
|
|
||||||
otherWalker.forward(tr, walker.index)
|
|
||||||
otherWalker.forward(tr, 0)
|
|
||||||
walker.forward(tr, 0)
|
|
||||||
t.assert(walker.index === i)
|
|
||||||
t.assert(walker.left === otherWalker.left)
|
|
||||||
t.assert(walker.right === otherWalker.right)
|
|
||||||
t.assert(walker.nextItem === otherWalker.nextItem)
|
|
||||||
t.assert(walker.reachedEnd === otherWalker.reachedEnd)
|
|
||||||
if (i > 0) {
|
|
||||||
walker.backward(tr, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
users.map(u => u.destroy())
|
users.map(u => u.destroy())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export const testUndoInEmbed = tc => {
|
|||||||
*/
|
*/
|
||||||
export const testUndoDeleteFilter = tc => {
|
export const testUndoDeleteFilter = tc => {
|
||||||
/**
|
/**
|
||||||
* @type {Array<Y.Map<any>>}
|
* @type {Y.Array<any>}
|
||||||
*/
|
*/
|
||||||
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
|
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
|
||||||
const undoManager = new Y.UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
|
const undoManager = new Y.UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
|
||||||
|
|||||||
@@ -1,10 +1,71 @@
|
|||||||
import { init, compare, applyRandomTests, Doc, AbstractType, TestConnector } from './testHelper.js' // eslint-disable-line
|
import { init, compare, applyRandomTests, Doc, Item } from './testHelper.js' // eslint-disable-line
|
||||||
|
|
||||||
import * as Y from '../src/index.js'
|
import * as Y from '../src/index.js'
|
||||||
import * as t from 'lib0/testing'
|
import * as t from 'lib0/testing'
|
||||||
import * as prng from 'lib0/prng'
|
import * as prng from 'lib0/prng'
|
||||||
import * as math from 'lib0/math'
|
import * as math from 'lib0/math'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* path should be correct when moving item - see yjs#481
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testArrayMovePathIssue481 = tc => {
|
||||||
|
const { users, testConnector, array0, array1 } = init(tc, { users: 2 })
|
||||||
|
array0.observeDeep(events => {
|
||||||
|
events.forEach(event => {
|
||||||
|
if (event.path.length > 0) {
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let target = event.currentTarget
|
||||||
|
event.path.forEach(p => {
|
||||||
|
target = target.get(p)
|
||||||
|
})
|
||||||
|
t.assert(target === event.target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
array0.push([
|
||||||
|
['a', '1.1'],
|
||||||
|
['b', '2.2'],
|
||||||
|
['c', '3.1'],
|
||||||
|
['d', '4.1'],
|
||||||
|
['e', '5.1']
|
||||||
|
].map(e => Y.Array.from(e)))
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[1].transact(() => {
|
||||||
|
array1.get(1).insert(0, ['0'])
|
||||||
|
array1.move(1, 0)
|
||||||
|
})
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[1].transact(() => {
|
||||||
|
array1.get(3).insert(0, ['1'])
|
||||||
|
array1.move(3, 4)
|
||||||
|
})
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[1].transact(() => {
|
||||||
|
array1.get(2).insert(0, ['2'])
|
||||||
|
array1.move(2, array1.length)
|
||||||
|
})
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* foreach has correct index - see yjs#485
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testArrayIndexIssue485 = tc => {
|
||||||
|
const doc = new Y.Doc()
|
||||||
|
const yarr = doc.getArray()
|
||||||
|
yarr.push([1, 2])
|
||||||
|
yarr.forEach((el, index) => {
|
||||||
|
t.info('index: ' + index)
|
||||||
|
t.assert(yarr.get(index) === el)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -472,6 +533,152 @@ export const testMove = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMove2 = tc => {
|
||||||
|
{
|
||||||
|
// move in uninitialized type
|
||||||
|
const yarr = new Y.Array()
|
||||||
|
yarr.insert(0, [1, 2])
|
||||||
|
yarr.move(1, 0)
|
||||||
|
// @ts-ignore
|
||||||
|
t.compare(yarr._prelimContent, [2, 1])
|
||||||
|
}
|
||||||
|
const { array0, array1, users } = init(tc, { users: 3 })
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let event0 = null
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let event1 = null
|
||||||
|
array0.observe(event => {
|
||||||
|
event0 = event
|
||||||
|
})
|
||||||
|
array1.observe(event => {
|
||||||
|
event1 = event
|
||||||
|
})
|
||||||
|
array0.insert(0, [1, 2])
|
||||||
|
array0.move(1, 0)
|
||||||
|
t.compare(array0.toArray(), [2, 1])
|
||||||
|
t.compare(event0.delta, [{ insert: [2] }, { retain: 1 }, { delete: 1 }])
|
||||||
|
Y.applyUpdate(users[1], Y.encodeStateAsUpdate(users[0]))
|
||||||
|
t.compare(array1.toArray(), [2, 1])
|
||||||
|
t.compare(event1.delta, [{ insert: [2, 1] }])
|
||||||
|
array0.move(0, 2)
|
||||||
|
t.compare(array0.toArray(), [1, 2])
|
||||||
|
t.compare(event0.delta, [{ delete: 1 }, { retain: 1 }, { insert: [2] }])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMoveSingleItemRemovesPrev = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yarray = ydoc.getArray()
|
||||||
|
yarray.insert(0, [1, 2, 3])
|
||||||
|
yarray.move(0, 3)
|
||||||
|
t.compareArrays(yarray.toArray(), [2, 3, 1])
|
||||||
|
yarray.move(2, 0)
|
||||||
|
t.compareArrays(yarray.toArray(), [1, 2, 3])
|
||||||
|
let item = yarray._start
|
||||||
|
const items = []
|
||||||
|
while (item) {
|
||||||
|
items.push(item)
|
||||||
|
item = item.right
|
||||||
|
}
|
||||||
|
t.assert(items.length === 4)
|
||||||
|
t.assert(items.filter(item => !item.deleted).length === 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the searchMarker is reused correctly.
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testListWalkerReusesSearchMarker = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yarray = ydoc.getArray()
|
||||||
|
const iterations = 100
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
yarray.insert(0, [i])
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let prevSm = null
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
const v = yarray.get(i)
|
||||||
|
t.assert(v === iterations - i - 1)
|
||||||
|
t.assert(yarray._searchMarker.length <= 1)
|
||||||
|
const sm = yarray._searchMarker[0]
|
||||||
|
t.assert(prevSm == null || sm === prevSm)
|
||||||
|
prevSm = sm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testMoveDeletions = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yarray = ydoc.getArray()
|
||||||
|
const array = yarray.toArray()
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let lastDelta = []
|
||||||
|
yarray.observe(event => {
|
||||||
|
lastDelta = event.delta
|
||||||
|
let pos = 0
|
||||||
|
for (let i = 0; i < lastDelta.length; i++) {
|
||||||
|
const d = lastDelta[i]
|
||||||
|
if (d.retain != null) {
|
||||||
|
pos += d.retain
|
||||||
|
} else if (d.insert instanceof Array) {
|
||||||
|
array.splice(pos, 0, ...d.insert)
|
||||||
|
pos += d.insert.length
|
||||||
|
} else if (d.delete != null) {
|
||||||
|
array.splice(pos, d.delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
yarray.insert(0, [1, 2, 3])
|
||||||
|
// @todo should be old-position to new-position. so that below move matches
|
||||||
|
yarray.move(2, 0)
|
||||||
|
t.compare(lastDelta, [{ insert: [3] }, { retain: 2 }, { delete: 1 }])
|
||||||
|
t.compareArrays(yarray.toArray(), [3, 1, 2])
|
||||||
|
t.compareArrays(yarray.toArray(), array)
|
||||||
|
ydoc.transact(tr => {
|
||||||
|
/** @type {Item} */ (yarray._start).delete(tr)
|
||||||
|
})
|
||||||
|
t.compare(lastDelta, [{ delete: 1 }, { retain: 2 }, { insert: [3] }])
|
||||||
|
t.compareArrays(yarray.toArray(), [1, 2, 3])
|
||||||
|
t.compareArrays(yarray.toArray(), array)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*
|
||||||
|
export const testMoveCircles = tc => {
|
||||||
|
const { testConnector, array0, array1 } = init(tc, { users: 3 })
|
||||||
|
array0.insert(0, [1, 2, 3, 4])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
array0.moveRange(0, 1, 3)
|
||||||
|
t.compare(array0.toArray(), [3, 1, 2, 4])
|
||||||
|
array1.moveRange(2, 3, 1)
|
||||||
|
t.compare(array1.toArray(), [1, 3, 4, 2])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(array0.length === 4)
|
||||||
|
t.assert(array0.length === array0.toArray().length)
|
||||||
|
t.compareArrays(array0.toArray(), array1.toArray())
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -496,8 +703,6 @@ const getUniqueNumber = () => _uniqueNumber++
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Array<function(Doc,prng.PRNG,any):void>}
|
* @type {Array<function(Doc,prng.PRNG,any):void>}
|
||||||
*
|
|
||||||
* @todo to replace content to a separate data structure so we know that insert & returns work as expected!!!
|
|
||||||
*/
|
*/
|
||||||
const arrayTransactions = [
|
const arrayTransactions = [
|
||||||
function move (user, gen) {
|
function move (user, gen) {
|
||||||
@@ -506,11 +711,15 @@ const arrayTransactions = [
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const pos = prng.int32(gen, 0, yarray.length - 1)
|
const pos = prng.int32(gen, 0, yarray.length - 1)
|
||||||
const newPos = prng.int32(gen, 0, yarray.length)
|
const len = 1 // prng.int32(gen, 1, math.min(3, yarray.length - pos))
|
||||||
|
const _newPosAdj = prng.int32(gen, 0, yarray.length - len)
|
||||||
|
// make sure that we don't insert in-between the moved range
|
||||||
|
const newPos = _newPosAdj + (_newPosAdj > pos ? len : 0)
|
||||||
const oldContent = yarray.toArray()
|
const oldContent = yarray.toArray()
|
||||||
|
// yarray.moveRange(pos, pos + len - 1, newPos)
|
||||||
yarray.move(pos, newPos)
|
yarray.move(pos, newPos)
|
||||||
const [x] = oldContent.splice(pos, 1)
|
const movedValues = oldContent.splice(pos, len)
|
||||||
oldContent.splice(pos < newPos ? newPos - 1 : newPos, 0, x)
|
oldContent.splice(pos < newPos ? newPos - len : newPos, 0, ...movedValues)
|
||||||
t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position
|
t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position
|
||||||
},
|
},
|
||||||
function insert (user, gen) {
|
function insert (user, gen) {
|
||||||
@@ -594,6 +803,7 @@ const monitorArrayTestObject = user => {
|
|||||||
arr.splice(currpos, d.delete)
|
arr.splice(currpos, d.delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.compare(arr, yarr.toArray())
|
||||||
})
|
})
|
||||||
return arr
|
return arr
|
||||||
}
|
}
|
||||||
@@ -606,6 +816,7 @@ const compareTestobjects = cmp => {
|
|||||||
for (let i = 0; i < arrs.length; i++) {
|
for (let i = 0; i < arrs.length; i++) {
|
||||||
const type = cmp.users[i].getArray('array')
|
const type = cmp.users[i].getArray('array')
|
||||||
t.compareArrays(arrs[i], type.toArray())
|
t.compareArrays(arrs[i], type.toArray())
|
||||||
|
t.compareArrays(arrs[i], Array.from(type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,91 +824,112 @@ const compareTestobjects = cmp => {
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests6 = tc => {
|
export const testRepeatGeneratingYarrayTests6 = tc => {
|
||||||
compareTestobjects(applyRandomTests(tc, arrayTransactions, 3, monitorArrayTestObject))
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 6, monitorArrayTestObject))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests10 = tc => {
|
||||||
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 10, monitorArrayTestObject))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests30 = tc => {
|
||||||
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 30, monitorArrayTestObject))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests35 = tc => {
|
||||||
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 35, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests40 = tc => {
|
export const testRepeatGeneratingYarrayTests40 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 40)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 40, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests42 = tc => {
|
export const testRepeatGeneratingYarrayTests42 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 42)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 42, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests43 = tc => {
|
export const testRepeatGeneratingYarrayTests43 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 43)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 43, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests44 = tc => {
|
export const testRepeatGeneratingYarrayTests44 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 44)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 44, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests45 = tc => {
|
export const testRepeatGeneratingYarrayTests45 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 45)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 45, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests46 = tc => {
|
export const testRepeatGeneratingYarrayTests46 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 46)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 46, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests300 = tc => {
|
export const testRepeatGeneratingYarrayTests300 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 300)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 300, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests400 = tc => {
|
export const testRepeatGeneratingYarrayTests400 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 400)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 400, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests500 = tc => {
|
export const testRepeatGeneratingYarrayTests500 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 500)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 500, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests600 = tc => {
|
export const testRepeatGeneratingYarrayTests600 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 600)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 600, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests1000 = tc => {
|
export const testRepeatGeneratingYarrayTests1000 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 1000)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 1000, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests1800 = tc => {
|
export const testRepeatGeneratingYarrayTests1800 = tc => {
|
||||||
applyRandomTests(tc, arrayTransactions, 1800)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 1800, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -705,7 +937,7 @@ export const testRepeatGeneratingYarrayTests1800 = tc => {
|
|||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests3000 = tc => {
|
export const testRepeatGeneratingYarrayTests3000 = tc => {
|
||||||
t.skip(!t.production)
|
t.skip(!t.production)
|
||||||
applyRandomTests(tc, arrayTransactions, 3000)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 3000, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -713,7 +945,7 @@ export const testRepeatGeneratingYarrayTests3000 = tc => {
|
|||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests5000 = tc => {
|
export const testRepeatGeneratingYarrayTests5000 = tc => {
|
||||||
t.skip(!t.production)
|
t.skip(!t.production)
|
||||||
applyRandomTests(tc, arrayTransactions, 5000)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 5000, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -721,5 +953,5 @@ export const testRepeatGeneratingYarrayTests5000 = tc => {
|
|||||||
*/
|
*/
|
||||||
export const testRepeatGeneratingYarrayTests30000 = tc => {
|
export const testRepeatGeneratingYarrayTests30000 = tc => {
|
||||||
t.skip(!t.production)
|
t.skip(!t.production)
|
||||||
applyRandomTests(tc, arrayTransactions, 30000)
|
compareTestobjects(applyRandomTests(tc, arrayTransactions, 30000, monitorArrayTestObject))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user