diff --git a/src/structs/ContentType.js b/src/structs/ContentType.js index cc079694..1613a11e 100644 --- a/src/structs/ContentType.js +++ b/src/structs/ContentType.js @@ -113,13 +113,15 @@ export class ContentType { // when removing weak links, remove references to them // from type they're pointing to const type = /** @type {WeakLink} */ (this.type); - if (type._linkedItem !== null && !type._linkedItem.deleted) { - const item = /** @type {Item} */ (type._linkedItem) + for (let item = type._firstItem; item !== null; item = item.right) { if (item.linked) { unlinkFrom(transaction, item, type) } - type._linkedItem = null + if (item === type._lastItem) { + break; + } } + type._firstItem = type._lastItem = null } let item = this.type._start diff --git a/src/structs/Item.js b/src/structs/Item.js index e8a91f32..f9e5d5d5 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -396,9 +396,15 @@ export class Item extends AbstractStruct { } if (this.content.constructor === ContentType && /** @type {ContentType} */ (this.content).type.constructor === YWeakLink) { // make sure that linked content is integrated first - const linkSource = /** @type {any} */ (this.content).type._id - if (linkSource.clock >= getState(store, linkSource.client)) { - return linkSource.client + const content = /** @type {ContentType} */ (this.content) + const link = /** @type {YWeakLink} */ (content.type) + 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 } } diff --git a/src/types/YArray.js b/src/types/YArray.js index 4e61e742..4ea94568 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -215,7 +215,7 @@ export class YArray extends AbstractType { return arrayWeakLink(transaction, this, index) }) } else { - throw new Error('todo') + throw new Error('cannot create a link to an YArray that has not been integrated into YDoc') } } diff --git a/src/types/YWeakLink.js b/src/types/YWeakLink.js index f1ee1c74..7fb637ea 100644 --- a/src/types/YWeakLink.js +++ b/src/types/YWeakLink.js @@ -10,7 +10,8 @@ import { callTypeObservers, YWeakLinkRefID, writeID, - readID + readID, + RelativePosition } from "../internals.js" /** @@ -36,13 +37,26 @@ export class YWeakLinkEvent extends YEvent { */ export class YWeakLink extends AbstractType { /** - * @param {ID} id - * @param {Item|GC|null} item + * @param {RelativePosition} start + * @param {RelativePosition} end + * @param {Item|null} firstItem + * @param {Item|null} lastItem */ - constructor(id, item) { + constructor(start, end, firstItem, lastItem) { super() - this._id = id - this._linkedItem = item + this._quoteStart = start + 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} */ deref() { - if (this._linkedItem !== null && this._linkedItem.constructor === Item) { - let item = this._linkedItem + if (this._firstItem !== null) { + let item = this._firstItem if (item.parentSub !== null) { - // for map types go to the most recent one while (item.right !== null) { item = item.right } - this._linkedItem = item + // we don't support quotations over maps + this._firstItem = item } - if (!item.deleted) { - return item.content.getContent()[0] + if (!this._firstItem.deleted) { + return this._firstItem.content.getContent()[0] } } + return undefined; } + + /** + * Returns an array of references to all elements quoted by current weak link. + * + * @return {Array} + */ + unqote() { + let result = /** @type {Array} */ ([]) + 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. @@ -84,19 +117,25 @@ export class YWeakLink extends AbstractType { // link may refer to a single element in multi-element block // in such case we need to cut of the linked element into a // separate block - let sourceItem = this._linkedItem !== null ? this._linkedItem : getItemCleanStart(transaction, this._id) - if (sourceItem.constructor === Item && sourceItem.parentSub !== null) { + let firstItem = this._firstItem !== null ? this._firstItem : getItemCleanStart(transaction, /** @type {ID} */ (this._quoteStart.item)) + 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 - while (sourceItem.right !== null) { - sourceItem = sourceItem.right + while (firstItem.right !== null) { + firstItem = firstItem.right } + lastItem = firstItem } - if (!sourceItem.deleted && sourceItem.length > 1) { - sourceItem = getItemCleanEnd(transaction, transaction.doc.store, createID(sourceItem.id.client, sourceItem.id.clock + 1)) - } - this._linkedItem = sourceItem - if (!sourceItem.deleted) { - createLink(transaction, /** @type {Item} */ (sourceItem), this) + this._firstItem = firstItem + this._lastItem = lastItem + + /** @type {Item|null} */ + let item = firstItem + 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} */ _copy () { - return new YWeakLink(this._id, this._linkedItem) + return new YWeakLink(this._quoteStart, this._quoteEnd, this._firstItem, this._lastItem) } /** * @return {YWeakLink} */ 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) { encoder.writeTypeRef(YWeakLinkRefID) - const flags = 0 // flags that could be used in the future - encoding.writeUint8(encoder.restEncoder, flags) - writeID(encoder.restEncoder, this._id) + const isSingle = this.isSingle() + const info = (isSingle ? 0 : 1) | (this._quoteStart.assoc >= 0 ? 2 : 0) | (this._quoteEnd.assoc >= 0 ? 4 :0) + 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} */ export const readYWeakLink = decoder => { - const flags = decoding.readUint8(decoder.restDecoder) - const id = readID(decoder.restDecoder) - return new YWeakLink(id, null) + const info = decoding.readUint8(decoder.restDecoder) + const isSingle = (info & 1) !== 1 + 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!') @@ -159,27 +207,43 @@ const lengthExceeded = error.create('Length exceeded!') * @param {number} index * @return {YWeakLink} */ -export const arrayWeakLink = (transaction, parent, index) => { - let item = parent._start - for (; item !== null; item = item.right) { - if (!item.deleted && item.countable) { - if (index < item.length) { +export const arrayWeakLink = (transaction, parent, index, length = 1) => { + let startItem = parent._start + for (;startItem !== null; startItem = startItem.right) { + if (!startItem.deleted && startItem.countable) { + if (index < startItem.length) { 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) { - 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 + break; } - 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) => { const item = parent._map.get(key) 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) { transact(parent.doc, (transaction) => { createLink(transaction, item, link)