basics of range based quotations

This commit is contained in:
Bartosz Sypytkowski 2023-07-07 10:12:30 +02:00
parent 337cc1e202
commit 02367b612b
4 changed files with 130 additions and 56 deletions

View File

@ -113,13 +113,15 @@ export class ContentType {
// when removing weak links, remove references to them // when removing weak links, remove references to them
// from type they're pointing to // from type they're pointing to
const type = /** @type {WeakLink<any>} */ (this.type); const type = /** @type {WeakLink<any>} */ (this.type);
if (type._linkedItem !== null && !type._linkedItem.deleted) { for (let item = type._firstItem; item !== null; item = item.right) {
const item = /** @type {Item} */ (type._linkedItem)
if (item.linked) { if (item.linked) {
unlinkFrom(transaction, item, type) unlinkFrom(transaction, item, type)
} }
type._linkedItem = null if (item === type._lastItem) {
break;
}
} }
type._firstItem = type._lastItem = null
} }
let item = this.type._start let item = this.type._start

View File

@ -396,9 +396,15 @@ export class Item extends AbstractStruct {
} }
if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) { if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) {
// make sure that linked content is integrated first // make sure that linked content is integrated first
const linkSource = /** @type {any} */ (this.content).type._id const content = /** @type {ContentType} */ (this.content)
if (linkSource.clock >= getState(store, linkSource.client)) { const link = /** @type {YWeakLink<any>} */ (content.type)
return linkSource.client const start = link._quoteStart.item
if (start !== null && start.clock >= getState(store, start.client)) {
return start.client
}
const end = link._quoteEnd.item
if (end !== null && end.clock >= getState(store, end.client)) {
return end.client
} }
} }

View File

@ -215,7 +215,7 @@ export class YArray extends AbstractType {
return arrayWeakLink(transaction, this, index) return arrayWeakLink(transaction, this, index)
}) })
} else { } else {
throw new Error('todo') throw new Error('cannot create a link to an YArray that has not been integrated into YDoc')
} }
} }

View File

@ -10,7 +10,8 @@ import {
callTypeObservers, callTypeObservers,
YWeakLinkRefID, YWeakLinkRefID,
writeID, writeID,
readID readID,
RelativePosition
} from "../internals.js" } from "../internals.js"
/** /**
@ -36,13 +37,26 @@ export class YWeakLinkEvent extends YEvent {
*/ */
export class YWeakLink extends AbstractType { export class YWeakLink extends AbstractType {
/** /**
* @param {ID} id * @param {RelativePosition} start
* @param {Item|GC|null} item * @param {RelativePosition} end
* @param {Item|null} firstItem
* @param {Item|null} lastItem
*/ */
constructor(id, item) { constructor(start, end, firstItem, lastItem) {
super() super()
this._id = id this._quoteStart = start
this._linkedItem = item this._quoteEnd = end
this._firstItem = firstItem
this._lastItem = lastItem
}
/**
* Check if current link contains only a single element.
*
* @returns {boolean}
*/
isSingle() {
return this._quoteStart.item === this._quoteEnd.item
} }
/** /**
@ -51,21 +65,40 @@ export class YWeakLink extends AbstractType {
* @return {T|undefined} * @return {T|undefined}
*/ */
deref() { deref() {
if (this._linkedItem !== null && this._linkedItem.constructor === Item) { if (this._firstItem !== null) {
let item = this._linkedItem let item = this._firstItem
if (item.parentSub !== null) { if (item.parentSub !== null) {
// for map types go to the most recent one
while (item.right !== null) { while (item.right !== null) {
item = item.right item = item.right
} }
this._linkedItem = item // we don't support quotations over maps
this._firstItem = item
} }
if (!item.deleted) { if (!this._firstItem.deleted) {
return item.content.getContent()[0] return this._firstItem.content.getContent()[0]
} }
} }
return undefined; return undefined;
} }
/**
* Returns an array of references to all elements quoted by current weak link.
*
* @return {Array<any>}
*/
unqote() {
let result = /** @type {Array<any>} */ ([])
let item = this._firstItem
//TODO: moved elements
while (item !== null && item !== this._lastItem) {
if (!item.deleted) {
result = result.concat(item.content.getContent())
}
item = item.right
}
return result
}
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
@ -84,19 +117,25 @@ export class YWeakLink extends AbstractType {
// link may refer to a single element in multi-element block // link may refer to a single element in multi-element block
// in such case we need to cut of the linked element into a // in such case we need to cut of the linked element into a
// separate block // separate block
let sourceItem = this._linkedItem !== null ? this._linkedItem : getItemCleanStart(transaction, this._id) let firstItem = this._firstItem !== null ? this._firstItem : getItemCleanStart(transaction, /** @type {ID} */ (this._quoteStart.item))
if (sourceItem.constructor === Item && sourceItem.parentSub !== null) { let lastItem = this._lastItem !== null ? this._lastItem : getItemCleanEnd(transaction, y.store, /** @type {ID} */(this._quoteEnd.item))
if (firstItem.parentSub !== null) {
// for maps, advance to most recent item // for maps, advance to most recent item
while (sourceItem.right !== null) { while (firstItem.right !== null) {
sourceItem = sourceItem.right firstItem = firstItem.right
} }
lastItem = firstItem
} }
if (!sourceItem.deleted && sourceItem.length > 1) { this._firstItem = firstItem
sourceItem = getItemCleanEnd(transaction, transaction.doc.store, createID(sourceItem.id.client, sourceItem.id.clock + 1)) this._lastItem = lastItem
}
this._linkedItem = sourceItem /** @type {Item|null} */
if (!sourceItem.deleted) { let item = firstItem
createLink(transaction, /** @type {Item} */ (sourceItem), this) for (; item !== null; item = item.right) {
createLink(transaction, item, this)
if (item === lastItem) {
break;
}
} }
}) })
} }
@ -106,14 +145,14 @@ export class YWeakLink extends AbstractType {
* @return {YWeakLink<T>} * @return {YWeakLink<T>}
*/ */
_copy () { _copy () {
return new YWeakLink(this._id, this._linkedItem) return new YWeakLink(this._quoteStart, this._quoteEnd, this._firstItem, this._lastItem)
} }
/** /**
* @return {YWeakLink<T>} * @return {YWeakLink<T>}
*/ */
clone () { clone () {
return new YWeakLink(this._id, this._linkedItem) return new YWeakLink(this._quoteStart, this._quoteEnd, this._firstItem, this._lastItem)
} }
/** /**
@ -132,9 +171,13 @@ export class YWeakLink extends AbstractType {
*/ */
_write (encoder) { _write (encoder) {
encoder.writeTypeRef(YWeakLinkRefID) encoder.writeTypeRef(YWeakLinkRefID)
const flags = 0 // flags that could be used in the future const isSingle = this.isSingle()
encoding.writeUint8(encoder.restEncoder, flags) const info = (isSingle ? 0 : 1) | (this._quoteStart.assoc >= 0 ? 2 : 0) | (this._quoteEnd.assoc >= 0 ? 4 :0)
writeID(encoder.restEncoder, this._id) encoding.writeUint8(encoder.restEncoder, info)
writeID(encoder.restEncoder, /** @type {ID} */ (this._quoteStart.item))
if (!isSingle) {
writeID(encoder.restEncoder, /** @type {ID} */ (this._quoteEnd.item))
}
} }
} }
@ -144,9 +187,14 @@ export class YWeakLink extends AbstractType {
* @return {YWeakLink<any>} * @return {YWeakLink<any>}
*/ */
export const readYWeakLink = decoder => { export const readYWeakLink = decoder => {
const flags = decoding.readUint8(decoder.restDecoder) const info = decoding.readUint8(decoder.restDecoder)
const id = readID(decoder.restDecoder) const isSingle = (info & 1) !== 1
return new YWeakLink(id, null) const startAssoc = (info & 2) === 2 ? 0 : -1
const endAssoc = (info & 4) === 4 ? 0 : -1
const startID = readID(decoder.restDecoder)
const start = new RelativePosition(null, null, startID, startAssoc)
const end = new RelativePosition(null, null, isSingle ? startID : readID(decoder.restDecoder), endAssoc)
return new YWeakLink(start, end, null, null)
} }
const lengthExceeded = error.create('Length exceeded!') const lengthExceeded = error.create('Length exceeded!')
@ -159,27 +207,43 @@ const lengthExceeded = error.create('Length exceeded!')
* @param {number} index * @param {number} index
* @return {YWeakLink<any>} * @return {YWeakLink<any>}
*/ */
export const arrayWeakLink = (transaction, parent, index) => { export const arrayWeakLink = (transaction, parent, index, length = 1) => {
let item = parent._start let startItem = parent._start
for (; item !== null; item = item.right) { for (;startItem !== null; startItem = startItem.right) {
if (!item.deleted && item.countable) { if (!startItem.deleted && startItem.countable) {
if (index < item.length) { if (index < startItem.length) {
if (index > 0) { if (index > 0) {
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + index)) startItem = getItemCleanStart(transaction, createID(startItem.id.client, startItem.id.clock + index))
} }
if (item.length > 1) { break;
item = getItemCleanEnd(transaction, transaction.doc.store, createID(item.id.client, item.id.clock))
}
const link = new YWeakLink(item.id, item)
if (parent.doc !== null) {
const source = /** @type {Item} */ (item)
transact(parent.doc, (transaction) => {
createLink(transaction, source, link)
})
}
return link
} }
index -= item.length index -= startItem.length
}
}
if (startItem !== null) {
let endItem = startItem
let remaining = length
for (;endItem !== null && endItem.right !== null && endItem.length > remaining; endItem = endItem.right) {
// iterate over the items to reach the last block in the quoted range
remaining -= endItem.length
}
if (endItem.length >= remaining) {
endItem = getItemCleanEnd(transaction, transaction.doc.store, createID(startItem.id.client, startItem.id.clock + remaining - 1))
const start = new RelativePosition(null, null, startItem.id, 0)
const end = new RelativePosition(null, null, endItem.lastId, -1)
const link = new YWeakLink(start, end, startItem, endItem)
if (parent.doc !== null) {
transact(parent.doc, (transaction) => {
for (let item = link._firstItem; item !== null; item = item = item.right) {
createLink(transaction, item, link)
if (item === link._lastItem) {
break;
}
}
})
}
return link
} }
} }
@ -196,7 +260,9 @@ export const arrayWeakLink = (transaction, parent, index) => {
export const mapWeakLink = (parent, key) => { export const mapWeakLink = (parent, key) => {
const item = parent._map.get(key) const item = parent._map.get(key)
if (item !== undefined) { if (item !== undefined) {
const link = new YWeakLink(item.id, item) const start = new RelativePosition(null, null, item.id, 0)
const end = new RelativePosition(null, null, item.id, -1)
const link = new YWeakLink(start, end, item, item)
if (parent.doc !== null) { if (parent.doc !== null) {
transact(parent.doc, (transaction) => { transact(parent.doc, (transaction) => {
createLink(transaction, item, link) createLink(transaction, item, link)