This commit is contained in:
Kevin Jahns 2022-07-11 18:35:16 +02:00
parent 3b31764b6e
commit 100e436e2c
15 changed files with 106 additions and 324 deletions

View File

@ -48,7 +48,6 @@ export {
findRootTypeKey, findRootTypeKey,
findIndexSS, findIndexSS,
getItem, getItem,
typeListToArraySnapshot,
typeMapGetSnapshot, typeMapGetSnapshot,
createDocFromSnapshot, createDocFromSnapshot,
iterateDeletedStructs, iterateDeletedStructs,

View File

@ -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/ListWalker.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'

View File

@ -13,31 +13,36 @@ import {
/** /**
* @param {ContentMove | { start: RelativePosition, end: RelativePosition }} moved * @param {ContentMove | { start: RelativePosition, end: RelativePosition }} moved
* @param {Transaction} tr * @param {Transaction} tr
* @param {boolean} split
* @return {{ start: Item, end: Item }} $start (inclusive) is the beginning and $end (inclusive) is the end of the moved area * @return {{ start: Item, end: Item }} $start (inclusive) is the beginning and $end (inclusive) is the end of the moved area
*/ */
export const getMovedCoords = (moved, tr) => { 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) // @todo Try using getItem after all tests succeed again. // 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 {
error.unexpectedCase() error.unexpectedCase()
@ -60,7 +65,7 @@ export const findMoveLoop = (tr, moved, movedItem, trackedMovedItems) => {
/** /**
* @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 ( if (
!start.deleted && !start.deleted &&
@ -152,10 +157,11 @@ export class ContentMove {
integrate (transaction, item) { integrate (transaction, item) {
const sm = /** @type {AbstractType<any>} */ (item.parent)._searchMarker const sm = /** @type {AbstractType<any>} */ (item.parent)._searchMarker
if (sm) sm.length = 0 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.
@ -169,7 +175,10 @@ export class ContentMove {
prevMove.deleteAsCleanup(transaction, adaptPriority) prevMove.deleteAsCleanup(transaction, adaptPriority)
} }
this.overrides.add(prevMove) this.overrides.add(prevMove)
transaction._mergeStructs.push(start) // @todo is this needed? if (start !== movedCoords.start) {
// only add this to mergeStructs if this is not the first item
transaction._mergeStructs.push(start)
}
} }
maxPriority = math.max(maxPriority, nextPrio) maxPriority = math.max(maxPriority, nextPrio)
// was already moved // was already moved
@ -201,7 +210,7 @@ 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) const prevMoved = transaction.prevMoved.get(start)
@ -268,7 +277,6 @@ 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}

View File

@ -762,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
@ -772,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
] ]

View File

@ -10,8 +10,8 @@ import {
createID, createID,
ContentAny, ContentAny,
ContentBinary, ContentBinary,
ListIterator, ListWalker,
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,25 +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(ListWalker):T} f
* @return T * @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) { // @todo add condition `index < 5` if (searchMarker === null || yarray._start === null || index < freshSearchMarkerDistance) {
return f(new ListIterator(yarray).forward(tr, index, true)) return f(new ListWalker(yarray).forward(tr, index, true))
} }
if (searchMarker.length === 0) { if (searchMarker.length === 0) {
const sm = new ListIterator(yarray).forward(tr, index, true) const sm = new ListWalker(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 // @todo use >= 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 ListWalker(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)
@ -61,14 +62,6 @@ export const useSearchMarker = (tr, yarray, index, f) => {
} else { } else {
fsm.forward(tr, -diff, true) fsm.forward(tr, -diff, true)
} }
// @todo remove this test
/*
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
@ -101,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<ListWalker>} 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 {ListWalker|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--) {
@ -197,7 +190,7 @@ export class AbstractType {
*/ */
this._dEH = createEventHandler() this._dEH = createEventHandler()
/** /**
* @type {null | Array<ListIterator>} * @type {null | Array<ListWalker>}
*/ */
this._searchMarker = null this._searchMarker = null
/** /**
@ -376,157 +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.
*
* @todo remove!
*
* @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
}
}
/**
*
* @todo remove!
*
* @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
}
/**
*
* @todo remove!
*
* @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
}
}
}
}
/**
*
* @todo remove!
*
* 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

View File

@ -8,7 +8,7 @@ import {
YArrayRefID, YArrayRefID,
callTypeObservers, callTypeObservers,
transact, transact,
ListIterator, ListWalker,
useSearchMarker, useSearchMarker,
createRelativePositionFromTypeIndex, createRelativePositionFromTypeIndex,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line
@ -45,7 +45,7 @@ export class YArray extends AbstractType {
*/ */
this._prelimContent = [] this._prelimContent = []
/** /**
* @type {Array<ListIterator>} * @type {Array<ListWalker>}
*/ */
this._searchMarker = [] this._searchMarker = []
} }
@ -141,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
@ -254,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 ListWalker(this).slice(tr, this.length)
) )
} }
@ -293,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 ListWalker(this).map(tr, f)
) )
} }
@ -304,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 ListWalker(this).forEach(tr, f)
) )
} }
@ -312,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()
} }

View File

@ -28,7 +28,7 @@ import {
ContentType, ContentType,
useSearchMarker, useSearchMarker,
findIndexCleanStart, findIndexCleanStart,
ListIterator, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line ListWalker, 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<ListWalker>}
*/ */
this._searchMarker = [] this._searchMarker = []
} }

View File

@ -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.

View File

@ -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
ListWalker
} 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 ListWalker(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)
) )

View File

@ -30,7 +30,7 @@ const lengthExceeded = error.create('Length exceeded!')
* computed item. * computed item.
* *
* @param {Transaction} tr * @param {Transaction} tr
* @param {ListIterator} li * @param {ListWalker} li
*/ */
const popMovedStack = (tr, li) => { const popMovedStack = (tr, li) => {
let { start, end, move } = li.movedStack.pop() || { start: null, end: null, move: null } let { start, end, move } = li.movedStack.pop() || { start: null, end: null, move: null }
@ -49,7 +49,7 @@ const popMovedStack = (tr, li) => {
) )
) )
) { ) {
const coords = getMovedCoords(moveContent, tr) const coords = getMovedCoords(moveContent, tr, false)
start = coords.start start = coords.start
end = coords.end end = coords.end
} }
@ -61,10 +61,9 @@ const popMovedStack = (tr, li) => {
} }
/** /**
* @todo rename to walker? * Structure that helps to iterate through list-like structures. This is a useful abstraction that keeps track of move operations.
* @todo check that inserting character one after another always reuses ListIterators
*/ */
export class ListIterator { export class ListWalker {
/** /**
* @param {AbstractType<any>} type * @param {AbstractType<any>} type
*/ */
@ -105,7 +104,7 @@ export class ListIterator {
} }
clone () { clone () {
const iter = new ListIterator(this.type) const iter = new ListWalker(this.type)
iter.index = this.index iter.index = this.index
iter.rel = this.rel iter.rel = this.rel
iter.nextItem = this.nextItem iter.nextItem = this.nextItem
@ -169,11 +168,6 @@ export class ListIterator {
} }
let item = /** @type {Item} */ (this.nextItem) let item = /** @type {Item} */ (this.nextItem)
this.index += len this.index += len
// @todo this condition is not needed, better to remove it (can always be applied)
if (this.rel) {
len += this.rel
this.rel = 0
}
// eslint-disable-next-line no-unmodified-loop-condition // 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)))) { 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)) { if (item === this.currMoveEnd || (this.currMoveEnd === null && this.reachedEnd && this.currMove)) {
@ -192,7 +186,7 @@ export class ListIterator {
if (this.currMove) { if (this.currMove) {
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove }) this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
} }
const { start, end } = getMovedCoords(item.content, tr) const { start, end } = getMovedCoords(item.content, tr, false)
this.currMove = item this.currMove = item
this.currMoveStart = start this.currMoveStart = start
this.currMoveEnd = end this.currMoveEnd = end
@ -205,7 +199,7 @@ export class ListIterator {
if (item.right) { if (item.right) {
item = item.right item = item.right
} else { } else {
this.reachedEnd = true // @todo we need to ensure to iterate further if this.currMoveEnd === null this.reachedEnd = true
} }
} }
this.index -= len this.index -= len
@ -250,7 +244,7 @@ export class ListIterator {
/** /**
* @param {Transaction} tr * @param {Transaction} tr
* @param {number} len * @param {number} len
* @return {ListIterator} * @return {ListWalker}
*/ */
backward (tr, len) { backward (tr, len) {
if (this.index - len < 0) { if (this.index - len < 0) {
@ -287,7 +281,7 @@ export class ListIterator {
if (this.currMove) { if (this.currMove) {
this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove }) this.movedStack.push({ start: this.currMoveStart, end: this.currMoveEnd, move: this.currMove })
} }
const { start, end } = getMovedCoords(item.content, tr) const { start, end } = getMovedCoords(item.content, tr, false)
this.currMove = item this.currMove = item
this.currMoveStart = start this.currMoveStart = start
this.currMoveEnd = end this.currMoveEnd = end
@ -336,7 +330,6 @@ export class ListIterator {
} }
if (nextItem.right) { if (nextItem.right) {
nextItem = nextItem.right nextItem = nextItem.right
this.nextItem = nextItem // @todo move this after the while loop
} else { } else {
this.reachedEnd = true this.reachedEnd = true
} }
@ -345,9 +338,6 @@ export class ListIterator {
// always set nextItem before any method call // always set nextItem before any method call
this.nextItem = nextItem this.nextItem = nextItem
this.forward(tr, 0, true) this.forward(tr, 0, true)
if (this.nextItem == null) {
throw new Error('debug me') // @todo remove
}
nextItem = this.nextItem nextItem = this.nextItem
} }
} }
@ -604,7 +594,7 @@ const concatArrayContent = (content, added) => {
* * Delete the stack-items that both of them have in common * * Delete the stack-items that both of them have in common
* *
* @param {Transaction} tr * @param {Transaction} tr
* @param {ListIterator} walker * @param {ListWalker} walker
* @param {number} len * @param {number} len
* @return {Array<{ start: RelativePosition, end: RelativePosition }>} * @return {Array<{ start: RelativePosition, end: RelativePosition }>}
*/ */
@ -713,7 +703,7 @@ export const getMinimalListViewRanges = (tr, walker, len) => {
const normalizedRanges = array.flatten(ranges.map(range => { const normalizedRanges = array.flatten(ranges.map(range => {
// A subset of a range could be moved by another move with a higher priority. // 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. // If that is the case, we need to ignore those moved items.
const { start, end } = getMovedCoords(range, tr) const { start, end } = getMovedCoords(range, tr, false)
const move = range.move const move = range.move
const ranges = [] const ranges = []
/** /**

View File

@ -226,9 +226,9 @@ export class YEvent {
} else if (item === null) { } else if (item === null) {
break break
} else if (item.content.constructor === ContentMove) { } else if (item.content.constructor === ContentMove) {
if (item.moved === currMove && (!item.deleted || (this.deletes(item) && !this.adds(item)))) { // @todo !item.deleted || this.deletes(item) if (item.moved === currMove && (!item.deleted || (this.deletes(item) && !this.adds(item)))) {
movedStack.push({ end: currMoveEnd, move: currMove, isNew: currMoveIsNew, isDeleted: currMoveIsDeleted }) 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 currMoveIsNew = this.adds(item) || currMoveIsNew

View File

@ -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

View File

@ -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

View File

@ -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())
} }

View File

@ -534,6 +534,32 @@ export const testMoveSingleItemRemovesPrev = tc => {
t.assert(items.filter(item => !item.deleted).length === 3) 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 * @param {t.TestCase} tc
*/ */
@ -617,8 +643,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) {
@ -732,6 +756,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))
} }
} }