implement search-marker prototype (limited usage for now)

This commit is contained in:
Kevin Jahns
2020-07-15 22:03:02 +02:00
parent 6e8167fe51
commit 6e3b708599
11 changed files with 628 additions and 187 deletions

View File

@@ -20,11 +20,14 @@ import {
splitSnapshotAffectedStructs,
iterateDeletedStructs,
iterateStructs,
AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
findMarker,
updateMarkerChanges,
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js'
import * as object from 'lib0/object.js'
import * as map from 'lib0/map.js'
import * as error from 'lib0/error.js'
/**
* @param {any} a
@@ -33,75 +36,79 @@ import * as map from 'lib0/map.js'
*/
const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
export class ItemListPosition {
/**
* @param {Item|null} left
* @param {Item|null} right
*/
constructor (left, right) {
this.left = left
this.right = right
}
}
export class ItemTextListPosition extends ItemListPosition {
export class ItemTextListPosition {
/**
* @param {Item|null} left
* @param {Item|null} right
* @param {number} index
* @param {Map<string,any>} currentAttributes
*/
constructor (left, right, currentAttributes) {
super(left, right)
constructor (left, right, index, currentAttributes) {
this.left = left
this.right = right
this.index = index
this.currentAttributes = currentAttributes
}
}
export class ItemInsertionResult extends ItemListPosition {
/**
* @param {Item|null} left
* @param {Item|null} right
* @param {Map<string,any>} negatedAttributes
* Only call this if you know that this.right is defined
*/
constructor (left, right, negatedAttributes) {
super(left, right)
this.negatedAttributes = negatedAttributes
forward () {
if (this.right === null) {
error.unexpectedCase()
}
switch (this.right.content.constructor) {
case ContentEmbed:
case ContentString:
if (!this.right.deleted) {
this.index += this.right.length
}
break
case ContentFormat:
if (!this.right.deleted) {
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
}
break
}
this.left = this.right
this.right = this.right.right
}
}
/**
* @param {Transaction} transaction
* @param {Map<string,any>} currentAttributes
* @param {Item|null} left
* @param {Item|null} right
* @param {number} count
* @param {ItemTextListPosition} pos
* @param {number} count steps to move forward
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findNextPosition = (transaction, currentAttributes, left, right, count) => {
while (right !== null && count > 0) {
switch (right.content.constructor) {
const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) {
case ContentEmbed:
case ContentString:
if (!right.deleted) {
if (count < right.length) {
if (!pos.right.deleted) {
if (count < pos.right.length) {
// split right
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + count))
getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count))
}
count -= right.length
pos.index += pos.right.length
count -= pos.right.length
}
break
case ContentFormat:
if (!right.deleted) {
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
}
left = right
right = right.right
pos.left = pos.right
pos.right = pos.right.right
// pos.forward() - we don't forward because that would halve the performance because we already do the checks above
}
return new ItemTextListPosition(left, right, currentAttributes)
return pos
}
/**
@@ -115,8 +122,14 @@ const findNextPosition = (transaction, currentAttributes, left, right, count) =>
*/
const findPosition = (transaction, parent, index) => {
const currentAttributes = new Map()
const right = parent._start
return findNextPosition(transaction, currentAttributes, null, right, index)
const marker = findMarker(parent, index)
if (marker) {
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
return findNextPosition(transaction, pos, index - marker.index)
} else {
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes)
return findNextPosition(transaction, pos, index)
}
}
/**
@@ -124,37 +137,35 @@ const findPosition = (transaction, parent, index) => {
*
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemListPosition} currPos
* @param {ItemTextListPosition} currPos
* @param {Map<string,any>} negatedAttributes
*
* @private
* @function
*/
const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => {
let { left, right } = currPos
// check if we really need to remove attributes
while (
right !== null && (
right.deleted === true || (
right.content.constructor === ContentFormat &&
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (right.content).key), /** @type {ContentFormat} */ (right.content).value)
currPos.right !== null && (
currPos.right.deleted === true || (
currPos.right.content.constructor === ContentFormat &&
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
)
)
) {
if (!right.deleted) {
negatedAttributes.delete(/** @type {ContentFormat} */ (right.content).key)
if (!currPos.right.deleted) {
negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key)
}
left = right
right = right.right
currPos.forward()
}
const doc = transaction.doc
const ownClientId = doc.clientID
let left = currPos.left
const right = currPos.right
negatedAttributes.forEach((val, key) => {
left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
left.integrate(transaction, 0)
})
currPos.left = left
currPos.right = right
}
/**
@@ -174,59 +185,51 @@ const updateCurrentAttributes = (currentAttributes, format) => {
}
/**
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {ItemTextListPosition} currPos
* @param {Object<string,any>} attributes
*
* @private
* @function
*/
const minimizeAttributeChanges = (currPos, currentAttributes, attributes) => {
const minimizeAttributeChanges = (currPos, attributes) => {
// go right while attributes[right.key] === right.value (or right is deleted)
let { left, right } = currPos
while (true) {
if (right === null) {
if (currPos.right === null) {
break
} else if (right.deleted) {
// continue
} else if (right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (right.content)).key] || null, /** @type {ContentFormat} */ (right.content).value)) {
// found a format, update currentAttributes and continue
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] || null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
//
} else {
break
}
left = right
right = right.right
currPos.forward()
}
currPos.left = left
currPos.right = right
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {ItemTextListPosition} currPos
* @param {Object<string,any>} attributes
* @return {Map<string,any>}
*
* @private
* @function
**/
const insertAttributes = (transaction, parent, currPos, currentAttributes, attributes) => {
const insertAttributes = (transaction, parent, currPos, attributes) => {
const doc = transaction.doc
const ownClientId = doc.clientID
const negatedAttributes = new Map()
// insert format-start items
for (const key in attributes) {
const val = attributes[key]
const currentVal = currentAttributes.get(key) || null
const currentVal = currPos.currentAttributes.get(key) || null
if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal)
const { left, right } = currPos
currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
currPos.left.integrate(transaction, 0)
currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
currPos.right.integrate(transaction, 0)
currPos.forward()
}
}
return negatedAttributes
@@ -235,56 +238,59 @@ const insertAttributes = (transaction, parent, currPos, currentAttributes, attri
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {ItemTextListPosition} currPos
* @param {string|object} text
* @param {Object<string,any>} attributes
*
* @private
* @function
**/
const insertText = (transaction, parent, currPos, currentAttributes, text, attributes) => {
currentAttributes.forEach((val, key) => {
const insertText = (transaction, parent, currPos, text, attributes) => {
currPos.currentAttributes.forEach((val, key) => {
if (attributes[key] === undefined) {
attributes[key] = null
}
})
const doc = transaction.doc
const ownClientId = doc.clientID
minimizeAttributeChanges(currPos, currentAttributes, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, currentAttributes, attributes)
minimizeAttributeChanges(currPos, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// insert content
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
const { left, right } = currPos
currPos.left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
currPos.left.integrate(transaction, 0)
return insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
let { left, right, index } = currPos
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
}
right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
right.integrate(transaction, 0)
currPos.right = right
currPos.index = index
currPos.forward()
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
}
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {ItemTextListPosition} currPos
* @param {number} length
* @param {Object<string,any>} attributes
*
* @private
* @function
*/
const formatText = (transaction, parent, currPos, currentAttributes, length, attributes) => {
const formatText = (transaction, parent, currPos, length, attributes) => {
const doc = transaction.doc
const ownClientId = doc.clientID
minimizeAttributeChanges(currPos, currentAttributes, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, currentAttributes, attributes)
let { left, right } = currPos
minimizeAttributeChanges(currPos, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null
while (length > 0 && right !== null) {
if (!right.deleted) {
switch (right.content.constructor) {
while (length > 0 && currPos.right !== null) {
if (!currPos.right.deleted) {
switch (currPos.right.content.constructor) {
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (right.content)
const { key, value } = /** @type {ContentFormat} */ (currPos.right.content)
const attr = attributes[key]
if (attr !== undefined) {
if (equalAttrs(attr, value)) {
@@ -292,22 +298,20 @@ const formatText = (transaction, parent, currPos, currentAttributes, length, att
} else {
negatedAttributes.set(key, value)
}
right.delete(transaction)
currPos.right.delete(transaction)
}
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
break
}
case ContentEmbed:
case ContentString:
if (length < right.length) {
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
}
length -= right.length
length -= currPos.right.length
break
}
}
left = right
right = right.right
currPos.forward()
}
// Quill just assumes that the editor starts with a newline and that it always
// ends with a newline. We only insert that newline when a new newline is
@@ -317,11 +321,10 @@ const formatText = (transaction, parent, currPos, currentAttributes, length, att
for (; length > 0; length--) {
newlines += '\n'
}
left = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentString(newlines))
left.integrate(transaction, 0)
currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines))
currPos.right.integrate(transaction, 0)
currPos.forward()
}
currPos.left = left
currPos.right = right
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
}
@@ -431,42 +434,39 @@ export const cleanupYTextFormatting = type => {
/**
* @param {Transaction} transaction
* @param {ItemListPosition} currPos
* @param {Map<string,any>} currentAttributes
* @param {ItemTextListPosition} currPos
* @param {number} length
* @return {ItemListPosition}
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const deleteText = (transaction, currPos, currentAttributes, length) => {
const startAttrs = map.copy(currentAttributes)
const deleteText = (transaction, currPos, length) => {
const startLength = length
const startAttrs = map.copy(currPos.currentAttributes)
const start = currPos.right
let { left, right } = currPos
while (length > 0 && right !== null) {
if (right.deleted === false) {
switch (right.content.constructor) {
case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
break
while (length > 0 && currPos.right !== null) {
if (currPos.right.deleted === false) {
switch (currPos.right.content.constructor) {
case ContentEmbed:
case ContentString:
if (length < right.length) {
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
}
length -= right.length
right.delete(transaction)
length -= currPos.right.length
currPos.right.delete(transaction)
break
}
}
left = right
right = right.right
currPos.forward()
}
if (start) {
cleanupFormattingGap(transaction, start, right, startAttrs, map.copy(currentAttributes))
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, map.copy(currPos.currentAttributes))
}
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length)
}
currPos.left = left
currPos.right = right
return currPos
}
@@ -729,6 +729,10 @@ export class YText extends AbstractType {
* @type {Array<function():void>?}
*/
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
/**
* @type {Array<ArraySearchMarker>}
*/
this._searchMarker = []
}
/**
@@ -765,6 +769,7 @@ export class YText extends AbstractType {
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs)
const event = new YTextEvent(this, transaction)
const doc = transaction.doc
// If a remote change happened, we try to cleanup potential formatting duplicates.
@@ -778,7 +783,7 @@ export class YText extends AbstractType {
}
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
// @ts-ignore
if (!item.deleted && item.content.constructor === ContentFormat) {
if (item.content.constructor === ContentFormat) {
foundFormattingItem = true
}
})
@@ -786,7 +791,17 @@ export class YText extends AbstractType {
break
}
}
transact(doc, t => {
if (!foundFormattingItem) {
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
if (item instanceof GC || foundFormattingItem) {
return
}
if (item.parent === this && item.content.constructor === ContentFormat) {
foundFormattingItem = true
}
})
}
transact(doc, (t) => {
if (foundFormattingItem) {
// If a formatting item was inserted, we simply clean the whole type.
// We need to compute currentAttributes for the current position anyway.
@@ -795,7 +810,7 @@ export class YText extends AbstractType {
// If no formatting attribute was inserted, we can make due with contextless
// formatting cleanups.
// Contextless: it is not necessary to compute currentAttributes for the affected position.
iterateDeletedStructs(t, transaction.deleteSet, item => {
iterateDeletedStructs(t, t.deleteSet, item => {
if (item instanceof GC) {
return
}
@@ -852,11 +867,7 @@ export class YText extends AbstractType {
applyDelta (delta, { sanitize = true } = {}) {
if (this.doc !== null) {
transact(this.doc, transaction => {
/**
* @type {ItemListPosition}
*/
const currPos = new ItemListPosition(null, this._start)
const currentAttributes = new Map()
const currPos = new ItemTextListPosition(null, this._start, 0, new Map())
for (let i = 0; i < delta.length; i++) {
const op = delta[i]
if (op.insert !== undefined) {
@@ -867,12 +878,12 @@ export class YText extends AbstractType {
// paragraphs, but nothing bad will happen.
const ins = (!sanitize && typeof op.insert === 'string' && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
if (typeof ins !== 'string' || ins.length > 0) {
insertText(transaction, this, currPos, currentAttributes, ins, op.attributes || {})
insertText(transaction, this, currPos, ins, op.attributes || {})
}
} else if (op.retain !== undefined) {
formatText(transaction, this, currPos, currentAttributes, op.retain, op.attributes || {})
formatText(transaction, this, currPos, op.retain, op.attributes || {})
} else if (op.delete !== undefined) {
deleteText(transaction, currPos, currentAttributes, op.delete)
deleteText(transaction, currPos, op.delete)
}
}
})
@@ -1004,13 +1015,13 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, this, index)
const pos = findPosition(transaction, this, index)
if (!attributes) {
attributes = {}
// @ts-ignore
currentAttributes.forEach((v, k) => { attributes[k] = v })
pos.currentAttributes.forEach((v, k) => { attributes[k] = v })
}
insertText(transaction, this, new ItemListPosition(left, right), currentAttributes, text, attributes)
insertText(transaction, this, pos, text, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
@@ -1034,8 +1045,8 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, this, index)
insertText(transaction, this, new ItemListPosition(left, right), currentAttributes, embed, attributes)
const pos = findPosition(transaction, this, index)
insertText(transaction, this, pos, embed, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
@@ -1057,8 +1068,7 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, this, index)
deleteText(transaction, new ItemListPosition(left, right), currentAttributes, length)
deleteText(transaction, findPosition(transaction, this, index), length)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
@@ -1082,11 +1092,11 @@ export class YText extends AbstractType {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, this, index)
if (right === null) {
const pos = findPosition(transaction, this, index)
if (pos.right === null) {
return
}
formatText(transaction, this, new ItemListPosition(left, right), currentAttributes, length, attributes)
formatText(transaction, this, pos, length, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))