diff --git a/attributing-content.md b/attributing-content.md index 5ed180b8..72783e28 100644 --- a/attributing-content.md +++ b/attributing-content.md @@ -116,16 +116,6 @@ deletions and insertions only, without Attributions). `AttributionManager` is an abstract class for mapping attributions. It is possible to highlight arbitrary content with this approach. -The next steps are to: - -- finish the implementation for Y.Map and Y.Xml* (which should be easy, compared -to Y.Map). -- Implement an AttributionManager-CRDT for the backend that sits there and -associates changes with users. -- use `getContent(attributionManager)` instead of `toDelta` in y-prosemirror. -Would like to make the attribution part of y-prosemirror, however Nick can also -use this approach to customly render the changes in ProseMirror. - The AttributionManager is encodes very efficiently. The ids are encoded using run-length encoding and the Attributes are de-duplicated and only encoded once. The above example encodes in 20 bytes. diff --git a/src/types/YText.js b/src/types/YText.js index 059e3284..79491279 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -448,7 +448,7 @@ const cleanupContextlessFormattingGap = (transaction, item) => { * * This function won't be exported anymore as soon as there is confidence that the YText type works as intended. * - * @param {YText} type + * @param {YText} type * @return {number} How many formatting attributes have been cleaned up. */ export const cleanupYTextFormatting = type => { @@ -485,7 +485,7 @@ export const cleanupYTextFormatting = type => { */ export const cleanupYTextAfterTransaction = transaction => { /** - * @type {Set} + * @type {Set>} */ const needFullCleanup = new Set() // check if another formatting item was inserted @@ -500,10 +500,10 @@ export const cleanupYTextAfterTransaction = transaction => { // cleanup in a new transaction transact(doc, (t) => { iterateStructsByIdSet(transaction, transaction.deleteSet, item => { - if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) { + if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) { return } - const parent = /** @type {YText} */ (item.parent) + const parent = /** @type {YText} */ (item.parent) if (item.content.constructor === ContentFormat) { needFullCleanup.add(parent) } else { @@ -588,12 +588,13 @@ const deleteText = (transaction, currPos, length) => { */ /** - * @extends YEvent + * @template {{ [key:string]: any } | AbstractType } TextEmbeds + * @extends YEvent> * Event that describes the changes on a YText type. */ export class YTextEvent extends YEvent { /** - * @param {YText} ytext + * @param {YText} ytext * @param {Transaction} transaction * @param {Set} subs The keys that changed */ @@ -620,12 +621,12 @@ export class YTextEvent extends YEvent { } /** - * @type {{added:Set,deleted:Set,keys:Map,delta:delta.TextDelta}} + * @type {{added:Set,deleted:Set,keys:Map,delta:delta.TextDelta}} */ get changes () { if (this._changes === null) { /** - * @type {{added:Set,deleted:Set,keys:Map,delta:delta.TextDelta}} + * @type {{added:Set,deleted:Set,keys:Map,delta:delta.TextDelta}} */ const changes = { keys: this.keys, @@ -642,7 +643,7 @@ export class YTextEvent extends YEvent { * Compute the changes in the delta format. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * - * @type {delta.TextDelta} + * @type {delta.TextDelta} * * @public */ @@ -754,6 +755,7 @@ export class YTextEvent extends YEvent { * block formats (format information on a paragraph), embeds (complex elements * like pictures and videos), and text formats (**bold**, *italic*). * + * @template {any} Embeds * @extends AbstractType */ export class YText extends AbstractType { @@ -811,7 +813,7 @@ export class YText extends AbstractType { * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * - * @return {YText} + * @return {YText} */ clone () { const text = new YText() @@ -916,14 +918,14 @@ export class YText extends AbstractType { * attribution `{ isDeleted: true, .. }`. * * @param {AbstractAttributionManager} am - * @return {import('../utils/Delta.js').TextDelta} The Delta representation of this type. + * @return {import('../utils/Delta.js').TextDelta< Embeds extends import('./AbstractType.js').AbstractType ? import('./AbstractType.js').DeepContent : Embeds >} The Delta representation of this type. * * @public */ getContentDeep (am = noAttributionsManager) { return this.getContent(am).map(d => - d instanceof delta.InsertStringOp && d.insert instanceof AbstractType - ? new delta.InsertStringOp(d.insert.getContent(am), d.attributes, d.attribution) + d instanceof delta.InsertEmbedOp && d.insert instanceof AbstractType + ? new delta.InsertEmbedOp(d.insert.getContent(am), d.attributes, d.attribution) : d ) } @@ -936,7 +938,7 @@ export class YText extends AbstractType { * attribution `{ isDeleted: true, .. }`. * * @param {AbstractAttributionManager} am - * @return {import('../utils/Delta.js').TextDelta} The Delta representation of this type. + * @return {import('../utils/Delta.js').TextDelta} The Delta representation of this type. * * @public */ diff --git a/src/utils/AttributionManager.js b/src/utils/AttributionManager.js index 48104a46..ecdf61e7 100644 --- a/src/utils/AttributionManager.js +++ b/src/utils/AttributionManager.js @@ -271,8 +271,9 @@ export class SnapshotAttributionManager { const inserts = createIdMap() const deletes = createIdMapFromIdSet(diffIdSet(nextSnapshot.ds, prevSnapshot.ds), [createAttributionItem('change', '')]) nextSnapshot.sv.forEach((clock, client) => { - inserts.add(client, 0, prevSnapshot.sv.get(client) || 0, []) - inserts.add(client, prevSnapshot.sv.get(client) || 0, clock, [createAttributionItem('change', '')]) + const prevClock = prevSnapshot.sv.get(client) || 0 + inserts.add(client, 0, prevClock, []) // content is included in prevSnapshot is rendered without attributes + inserts.add(client, prevClock, clock - prevClock, [createAttributionItem('change', '')]) // content is rendered as "inserted" }) this.attrs = mergeIdMaps([diffIdMap(inserts, prevSnapshot.ds), deletes]) } @@ -289,10 +290,12 @@ export class SnapshotAttributionManager { let content = slice.length === 1 ? item.content : item.content.copy() slice.forEach(s => { const deleted = this.nextSnapshot.ds.has(item.id.client, s.clock) + const nonExistend = (this.nextSnapshot.sv.get(item.id.client) ?? 0) <= s.clock const c = content if (s.len < c.getLength()) { content = c.splice(s.len) } + if (nonExistend) return if (!deleted || (s.attrs != null && s.attrs.length > 0)) { let attrsWithoutChange = s.attrs?.filter(attr => attr.name !== 'change') ?? null if (s.attrs?.length === 0) { diff --git a/src/utils/Delta.js b/src/utils/Delta.js index dace41f7..f8ff0089 100644 --- a/src/utils/Delta.js +++ b/src/utils/Delta.js @@ -192,10 +192,6 @@ export class RetainOp { } } -/** - * @typedef {string | { [key: string]: any }} TextDeltaContent - */ - /** * @typedef {(TextDelta | ArrayDelta)} Delta */ diff --git a/tests/testHelper.js b/tests/testHelper.js index b9f3748a..37792c6d 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -465,7 +465,7 @@ export const compare = users => { const userArrayValues = users.map(u => u.getArray('array').toJSON()) const userMapValues = users.map(u => u.getMap('map').toJSON()) const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString()) - const userTextValues = users.map(u => u.getText('text').getContent()) + const userTextValues = users.map(u => u.getText('text').getContentDeep()) for (const u of users) { t.assert(u.store.pendingDs === null) t.assert(u.store.pendingStructs === null) diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index b37cc459..acd4f286 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -1872,7 +1872,7 @@ export const testSnapshotDeleteAfter = tc => { }, { insert: 'e' }]) - const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1, snapshot1)) + const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1)) t.compare(state1, delta.fromJSON([{ insert: 'abcd' }])) }