renamed TwoSetRenderer => AttributionsRenderer, AttributionsRenderer refactor

This commit is contained in:
Kevin Jahns
2026-07-11 23:59:37 +02:00
parent ad248577a0
commit 486d5677d9
11 changed files with 411 additions and 110 deletions

View File

@@ -18,7 +18,7 @@ export { Transaction, transact, cleanupYTextFormatting } from './utils/Transacti
export { UndoManager, undoContentIds } from './utils/UndoManager.js'
export { logUpdate, logUpdateV2, decodeUpdate, decodeUpdateV2, encodeStateVectorFromUpdate, encodeStateVectorFromUpdateV2, convertUpdateFormatV1ToV2, convertUpdateFormatV2ToV1, obfuscateUpdate, obfuscateUpdateV2, createContentIdsFromUpdate, createContentIdsFromUpdateV2, intersectUpdateWithContentIds, intersectUpdateWithContentIdsV2 } from './utils/updates.js'
export { YEvent, getPathTo } from './utils/YEvent.js'
export { TwosetRenderer, baseRenderer, AbstractRenderer, createDiffRenderer, DiffRenderer, createSnapshotRenderer, SnapshotRenderer, Attributions, $renderer } from './utils/Renderer.js'
export { AttributionsRenderer, createAttributionsRenderer, AbstractRenderer, createDiffRenderer, DiffRenderer, createSnapshotRenderer, SnapshotRenderer, $renderer } from './utils/Renderer.js'
export { diffDocsToDelta } from './utils/delta-helpers.js'
export { YType as Type, getTypeChildren, typeMapGetSnapshot, typeMapGetAllSnapshot, $ytype, $ytypeAny } from './ytype.js'
export { AbstractStruct } from './structs/AbstractStruct.js'

View File

@@ -13,28 +13,87 @@ import { UndoManager, StackItem } from './UndoManager.js'
import { $renderer, AttributedContent } from './renderer-helpers.js'
export { baseRenderer, AbstractRenderer, rendererContentLength, $renderer } from './renderer-helpers.js'
export { AbstractRenderer, rendererContentLength, $renderer } from './renderer-helpers.js'
/**
* Emit a single content piece into `contents`, applying the rendering rules of
* {@link AttributionsRenderer}. `inRC` is whether the piece is in `renderedContent` (renders normally,
* even when the item is deleted — a "restore"); `attrs` is its attribution (or `null`).
*
* @param {Array<AttributedContent<any>>} contents - where to write the result
* @param {AbstractContent} c
* @param {number} clock
* @param {boolean} deleted - the item's doc deleted flag
* @param {boolean} inRC - whether this piece is in `renderedContent`
* @param {Array<ContentAttribute<any>>|null} attrs
* @param {0|1|2|3} shouldRender - see {@link AbstractRenderer#readContent}
*/
const pushAttributedPiece = (contents, c, clock, deleted, inRC, attrs, shouldRender) => {
if (inRC || attrs != null) {
// Visible: rendered normally (in `renderedContent`) and/or attributed. A restored deletion
// (`inRC`) renders as alive, so its effective `deleted` flag is cleared.
contents.push(new AttributedContent(c, clock, deleted && !inRC, attrs, shouldRender))
} else if (deleted && shouldRender !== 0 && shouldRender !== 3) {
// Genuinely deleted, unattributed, not restored: preserve the legacy retain/delete-op
// behavior (mode 3 renders unattributed deleted content as nothing — see DiffRenderer).
contents.push(new AttributedContent(c, clock, true, null, shouldRender))
}
// else: hidden alive content (or invisible deleted content under mode 0/3) — emit nothing.
}
/**
* Renders content with attributions, given a single `attributions` {@link ContentMap} of how to
* attribute content and an optional `renderedContent` {@link IdSet} of what renders.
*
* - `attributions` (inserts deletes) is merged into a single `renderAs` map; content it covers
* always renders, carrying its attribution.
* - `renderedContent` defines the content that renders *normally* — even if the item is marked
* deleted in the doc (a "restore"). It defaults to the doc's alive content (`inserts deletes`),
* applied implicitly (a piece is in the default set ⟺ its item is not `deleted`), so the common
* "pure attribution overlay" case does no extra work. When a custom set is supplied it is
* authoritative: alive content omitted from it (and unattributed) is hidden.
*
* Restoring deleted content requires `gc: false` on the doc (the renderer does not rehydrate
* garbage-collected content, unlike {@link DiffRenderer}).
*
* @implements AbstractRenderer
*
* @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>}
*/
export class TwosetRenderer extends ObservableV2 {
export class AttributionsRenderer extends ObservableV2 {
/**
* @param {IdMap<any>} inserts
* @param {IdMap<any>} deletes
* @param {ContentMap} attributions - how to attribute content (`{ inserts, deletes }` IdMaps)
* @param {Object} [options]
* @param {IdSet?} [options.renderedContent] - content that renders normally; defaults to the
* doc's alive content (`inserts deletes`), applied implicitly.
*/
constructor (inserts, deletes) {
constructor (attributions, { renderedContent = null } = {}) {
super()
this.inserts = inserts
this.deletes = deletes
/**
* Raw coverage of the two maps — `readContent` remains authoritative for what actually
* renders. See {@link AbstractRenderer#attributed}.
* The two attribution maps merged into one — `readContent` consults this for how to attribute
* a piece.
* @type {IdMap<any>}
*/
this.renderAs = mergeIdMaps([attributions.inserts, attributions.deletes])
/**
* Coverage of `renderAs` (ids that carry attributions). The small set `hasItem` checks. May
* over-approximate what actually renders — `readContent` remains authoritative. See
* {@link AbstractRenderer#attributed}.
* @type {IdSet}
*/
this.attributed = mergeIdSets([createIdSetFromIdMap(inserts), createIdSetFromIdMap(deletes)])
this.attributed = createIdSetFromIdMap(this.renderAs)
/**
* Custom "content that renders normally" set, or `null` to use the implicit default
* (alive ⟺ `!item.deleted`).
* @type {IdSet?}
*/
this.renderedContent = renderedContent
/**
* Union of `renderedContent` and `attributed` — the single cheap "does this produce visible
* output?" set. Only materialized when a custom `renderedContent` is supplied.
* @type {IdSet?}
*/
this.rendered = renderedContent === null ? null : mergeIdSets([renderedContent, this.attributed])
}
get $type () { return $renderer }
@@ -44,7 +103,14 @@ export class TwosetRenderer extends ObservableV2 {
* @return {boolean}
*/
hasItem (item) {
return this.attributed.intersects(item.id.client, item.id.clock, item.length)
const { client, clock } = item.id
if (this.attributed.intersects(client, clock, item.length)) return true
if (this.renderedContent === null) return false
// Custom renderedContent differs from the generic fast path for: deleted content that must be
// restored, and alive content that must be hidden (not fully covered).
return item.deleted
? this.renderedContent.intersects(client, clock, item.length)
: !this.renderedContent.covers(client, clock, item.length)
}
/**
@@ -56,18 +122,49 @@ export class TwosetRenderer extends ObservableV2 {
* @param {0|1|2|3} shouldRender - whether this should render or just result in a `retain` operation (see AbstractRenderer#readContent)
*/
readContent (contents, client, clock, deleted, content, shouldRender) {
const slice = (deleted ? this.deletes : this.inserts).slice(client, clock, content.getLength())
content = slice.length === 1 ? content : content.copy()
slice.forEach(s => {
const c = content
if (s.len < c.getLength()) {
content = c.splice(s.len)
const total = content.getLength()
/**
* Segments of the item partitioned by `renderedContent`, or `null` when `inRC` is uniform over
* the whole item (the common case: default set, or a custom set that fully contains/excludes
* the item). `null` lets us slice `renderAs` once.
* @type {Array<import('./ids.js').MaybeIdRange>?}
*/
let outer = null
let inRC = !deleted // implicit default: alive ⟺ rendered normally
if (this.renderedContent !== null) {
outer = this.renderedContent.slice(client, clock, total)
if (outer.length === 1) {
inRC = outer[0].exists
outer = null
}
// see DiffRenderer#readContent: mode 3 renders unattributed deleted content as nothing
if (!deleted || s.attrs != null || (shouldRender !== 0 && shouldRender !== 3)) {
contents.push(new AttributedContent(c, s.clock, deleted, s.attrs, shouldRender))
}
if (outer === null) {
// Uniform `inRC` — a single `renderAs` slice fixes the attribution boundaries.
const slice = this.renderAs.slice(client, clock, total)
let rest = slice.length === 1 ? content : content.copy()
for (let i = 0; i < slice.length; i++) {
const s = slice[i]
const c = rest
if (i < slice.length - 1) rest = c.splice(s.len)
pushAttributedPiece(contents, c, s.clock, deleted, inRC, s.attrs, shouldRender)
}
})
return
}
// General case: partition by `renderedContent` (outer, fixes `inRC`/effectiveDeleted), then by
// `renderAs` (inner, fixes attrs). Multiple outer segments ⇒ multi-id (string-like) content, so
// copying to avoid mutating the caller's content is safe.
let rest = content.copy()
for (let oi = 0; oi < outer.length; oi++) {
const rcSeg = outer[oi]
const slice = this.renderAs.slice(client, rcSeg.clock, rcSeg.len)
for (let ii = 0; ii < slice.length; ii++) {
const s = slice[ii]
const last = oi === outer.length - 1 && ii === slice.length - 1
const c = rest
if (!last) rest = c.splice(s.len)
pushAttributedPiece(contents, c, s.clock, deleted, rcSeg.exists, s.attrs, shouldRender)
}
}
}
/**
@@ -77,14 +174,25 @@ export class TwosetRenderer extends ObservableV2 {
contentLength (item) {
if (!item.content.isCountable()) {
return 0
} else if (!item.deleted) {
return item.length
} else {
return this.deletes.sliceId(item.id, item.length).reduce((len, s) => s.attrs != null ? len + s.len : len, 0)
}
const { client, clock } = item.id
if (this.renderedContent === null) {
// Default: alive content renders in full; deleted content only where attributed.
return item.deleted ? this.renderAs.coveredLength(client, clock, item.length) : item.length
}
// Custom: rendered length is |item ∩ rendered| (renderedContent attributed).
return /** @type {IdSet} */ (this.rendered).coveredLength(client, clock, item.length)
}
}
/**
* @param {ContentMap} attributions - how to attribute content (`{ inserts, deletes }` IdMaps)
* @param {Object} [options]
* @param {IdSet?} [options.renderedContent] - content that renders normally; defaults to the doc's
* alive content (`inserts deletes`).
*/
export const createAttributionsRenderer = (attributions, options) => new AttributionsRenderer(attributions, options)
/**
* @param {StructStore} store
* @param {number} client
@@ -197,13 +305,6 @@ const collectSuggestedChanges = (tr, renderer, start, end, collectAll) => {
return { inserts, deletes }
}
export class Attributions {
constructor () {
this.inserts = createIdMap()
this.deletes = createIdMap()
}
}
/**
* @param {IdMap<any>|undefined} attrs
* @param {IdSet} slice
@@ -221,9 +322,9 @@ export class DiffRenderer extends ObservableV2 {
* @param {Doc} prevDoc
* @param {Doc} nextDoc
* @param {Object} [options] - options for the renderer
* @param {Attributions?} [options.attrs] - the attributes to apply to the diff
* @param {ContentMap?} [options.attributions] - the attributions to apply to the diff
*/
constructor (prevDoc, nextDoc, { attrs = null } = {}) {
constructor (prevDoc, nextDoc, { attributions = null } = {}) {
super()
const _nextDocInserts = createInsertSetFromStructStore(nextDoc.store, false) // unmaintained
const _prevDocInserts = createInsertSetFromStructStore(prevDoc.store, false) // unmaintained
@@ -231,8 +332,8 @@ export class DiffRenderer extends ObservableV2 {
const prevDocDeletes = createDeleteSetFromStructStore(prevDoc.store) // maintained
const insertDiff = diffIdSet(_nextDocInserts, _prevDocInserts)
const deleteDiff = diffIdSet(nextDocDeletes, prevDocDeletes)
this.inserts = extractAttributions(attrs?.inserts, insertDiff)
this.deletes = extractAttributions(attrs?.deletes, deleteDiff)
this.inserts = extractAttributions(attributions?.inserts, insertDiff)
this.deletes = extractAttributions(attributions?.deletes, deleteDiff)
/**
* Raw coverage of `inserts` `deletes`, maintained alongside them. Over-approximates the
* actually-rendered set (e.g. it keeps suggested-inserts that were deleted later) —
@@ -247,10 +348,10 @@ export class DiffRenderer extends ObservableV2 {
this._nextBOH = nextDoc.on('beforeObserverCalls', tr => {
// update inserts
const diffInserts = diffIdSet(tr.insertSet, _prevDocInserts)
insertIntoIdMap(this.inserts, extractAttributions(attrs?.inserts, diffInserts))
insertIntoIdMap(this.inserts, extractAttributions(attributions?.inserts, diffInserts))
// update deletes
const diffDeletes = diffIdSet(diffIdSet(tr.deleteSet, prevDocDeletes), this.inserts)
insertIntoIdMap(this.deletes, extractAttributions(attrs?.deletes, diffDeletes))
insertIntoIdMap(this.deletes, extractAttributions(attributions?.deletes, diffDeletes))
insertIntoIdSet(this.attributed, diffInserts)
insertIntoIdSet(this.attributed, diffDeletes)
// @todo fire update ranges on `diffInserts` and `diffDeletes`
@@ -458,7 +559,7 @@ export class DiffRenderer extends ObservableV2 {
* @param {Doc} prevDoc
* @param {Doc} nextDoc
* @param {Object} [options] - options for the renderer
* @param {ContentMap?} [options.attrs] - the attributes to apply to the diff
* @param {ContentMap?} [options.attributions] - the attributions to apply to the diff
*/
export const createDiffRenderer = (prevDoc, nextDoc, options) => new DiffRenderer(prevDoc, nextDoc, options)

View File

@@ -229,6 +229,60 @@ export class IdSet {
return false
}
/**
* Whether the entire range `[clock, clock+len)` of `client` is contained in this set.
* Allocation-free (binary search). Because `getIds()` returns maximal, merged ranges, full
* coverage of a range implies it lies within a single range.
*
* @param {number} client
* @param {number} clock
* @param {number} len
* @return {boolean}
*/
covers (client, clock, len) {
const dr = this.clients.get(client)
if (dr) {
const ranges = dr.getIds()
const index = findIndexInIdRanges(ranges, clock)
if (index !== null) {
const r = ranges[index]
return clock + len <= r.clock + r.len
}
}
return false
}
/**
* Total number of ids in `[clock, clock+len)` of `client` contained in this set.
* Allocation-free (binary search + linear scan of the overlapping ranges). Equivalent to
* `slice(client, clock, len).reduce((s, r) => r.exists ? s + r.len : s, 0)` without allocating.
*
* @param {number} client
* @param {number} clock
* @param {number} len
* @return {number}
*/
coveredLength (client, clock, len) {
const dr = this.clients.get(client)
let covered = 0
if (dr) {
const ranges = dr.getIds()
const end = clock + len
let index = findRangeStartInIdRanges(ranges, clock)
if (index !== null) {
for (; index < ranges.length; index++) {
const r = ranges[index]
if (r.clock >= end) break
const from = r.clock < clock ? clock : r.clock
const rEnd = r.clock + r.len
const to = rEnd > end ? end : rEnd
covered += to - from
}
}
}
return covered
}
/**
* Return slices of ids that exist in this idset.
*
@@ -1148,6 +1202,38 @@ export class IdMap {
return false
}
/**
* Total number of ids in `[clock, clock+len)` of `client` contained in this map (i.e. carrying
* an attribution, incl. an empty `[]`). Allocation-free (binary search + linear scan).
* Equivalent to `slice(client, clock, len).reduce((s, r) => r.attrs != null ? s + r.len : s, 0)`
* without allocating.
*
* @param {number} client
* @param {number} clock
* @param {number} len
* @return {number}
*/
coveredLength (client, clock, len) {
const dr = this.clients.get(client)
let covered = 0
if (dr) {
const ranges = dr.getIds()
const end = clock + len
let index = findRangeStartInIdRanges(ranges, clock)
if (index !== null) {
for (; index < ranges.length; index++) {
const r = ranges[index]
if (r.clock >= end) break
const from = r.clock < clock ? clock : r.clock
const rEnd = r.clock + r.len
const to = rEnd > end ? end : rEnd
covered += to - from
}
}
}
return covered
}
/**
* Return attributions for a slice of ids.
*

View File

@@ -11,6 +11,7 @@ import {
diffIdMap,
mergeIdMaps,
createIdSet,
createIdMap,
mergeIdSets,
diffIdSet,
createIdMapFromIdSet,
@@ -88,11 +89,11 @@ export const mergeContentIds = contents => createContentIds(
)
/**
* @param {IdMap<any>} inserts
* @param {IdMap<any>} deletes
* @param {IdMap<any>} [inserts]
* @param {IdMap<any>} [deletes]
* @return {ContentMap}
*/
export const createContentMap = (inserts, deletes) => ({ inserts, deletes })
export const createContentMap = (inserts = createIdMap(), deletes = createIdMap()) => ({ inserts, deletes })
/**
* @param {ContentIds} contentIds

View File

@@ -109,16 +109,6 @@ export class AbstractRenderer extends ObservableV2 {
export const $renderer = AbstractRenderer.prototype.$type = s.$type('y:r', AbstractRenderer)
/**
* The absence of a renderer: content renders as-is via the generic fast path, without any
* attribution lookups.
*
* @deprecated pass `null` (or omit the renderer option) instead — kept as an alias for downstream
* code that referenced the former base-renderer object.
* @type {null}
*/
export const baseRenderer = null
/**
* Rendered length of `item` under `renderer`: the generic rule — alive countable content renders
* at full length, everything else at length `0` — unless the renderer claims the item.

View File

@@ -242,6 +242,35 @@ export const testRepeatRandomIntersects = tc => {
compareIdSets(diffed1, altDiffed1)
}
/**
* The allocation-free `covers` / `coveredLength` must agree with per-id `has` and with the
* allocating `slice(...)` reduction.
*
* @param {t.TestCase} tc
*/
export const testRepeatRandomCoversAndCoveredLength = tc => {
const clients = 4
const clockRange = 100
const ids = createRandomIdSet(tc.prng, clients, clockRange)
for (let iter = 0; iter < 300; iter++) {
const client = prng.int31(tc.prng, 0, clients - 1)
const clock = prng.int31(tc.prng, 0, clockRange)
const len = prng.int31(tc.prng, 1, clockRange - clock + 5) // may exceed the range to cover edges
// reference by scanning every id in the range
let refCovered = 0
let refCovers = true
for (let c = clock; c < clock + len; c++) {
if (ids.has(client, c)) refCovered++
else refCovers = false
}
t.assert(ids.covers(client, clock, len) === refCovers)
t.assert(ids.coveredLength(client, clock, len) === refCovered)
// cross-check against the allocating slice
const sliceCovered = ids.slice(client, clock, len).reduce((s, r) => r.exists ? s + r.len : s, 0)
t.assert(ids.coveredLength(client, clock, len) === sliceCovered)
}
}
/**
* The struct that covers `clock` for the document's local client.
*

View File

@@ -142,7 +142,7 @@ export const testChildListContent = () => {
export const testAttributionSession1 = tc => {
const { testConnector, users, text0, text1 } = init(tc, { users: 3 })
users[0].gc = false
const globalAttributions = new Y.Attributions()
const globalAttributions = Y.createContentMap()
const v1 = Y.cloneDoc(users[0])
users.forEach(user => user.on('update', (update, _, ydoc, tr) => {
if (!tr.local) return
@@ -154,20 +154,20 @@ export const testAttributionSession1 = tc => {
text0.insert(0, 'a')
text1.insert(0, 'b')
testConnector.flushAllMessages()
const d1 = text0.toDelta({ renderer: Y.createDiffRenderer(v1, users[0], { attrs: globalAttributions }) })
const d1 = text0.toDelta({ renderer: Y.createDiffRenderer(v1, users[0], { attributions: globalAttributions }) })
t.compare(d1, delta.create().insert('a', null, { insert: ['0'] }).insert('b', null, { insert: ['1'] }).done())
const v2 = Y.cloneDoc(users[0])
text0.delete(1, 1)
text1.insert(2, 'c')
testConnector.flushAllMessages()
const d2 = text0.toDelta({ renderer: Y.createDiffRenderer(v2, users[0], { attrs: globalAttributions }) })
const d2 = text0.toDelta({ renderer: Y.createDiffRenderer(v2, users[0], { attributions: globalAttributions }) })
t.compare(d2, delta.create().insert('a').insert('b', null, { delete: ['0'] }).insert('c', null, { insert: ['1'] }).done())
const onlyUser0ChangesAttributed = {
inserts: Y.filterIdMap(globalAttributions.inserts, attrs => attrs.some(attr => attr.name === 'insert' && attr.val === '0')),
deletes: Y.filterIdMap(globalAttributions.deletes, attrs => attrs.some(attr => attr.name === 'delete' && attr.val === '0'))
}
const rendererUser0 = new Y.TwosetRenderer(onlyUser0ChangesAttributed.inserts, onlyUser0ChangesAttributed.deletes)
const onlyUser0ChangesAttributed = Y.createContentMap(
Y.filterIdMap(globalAttributions.inserts, attrs => attrs.some(attr => attr.name === 'insert' && attr.val === '0')),
Y.filterIdMap(globalAttributions.deletes, attrs => attrs.some(attr => attr.name === 'delete' && attr.val === '0'))
)
const rendererUser0 = new Y.AttributionsRenderer(onlyUser0ChangesAttributed)
const d3 = text0.toDelta({ renderer: rendererUser0 })
t.compare(d3, delta.create().insert('a', null, { insert: ['0'] }).insert('b', null, { delete: ['0'] }).insert('c').done())
Y.undoContentIds(users[0], Y.createContentIdsFromContentMap(onlyUser0ChangesAttributed))
@@ -701,17 +701,17 @@ export const testRdtDeltaAttributionSanity = () => {
const ydoc = new Y.Doc()
const root = ydoc.get('root')
const v1 = Y.cloneDoc(ydoc)
const attrs = new Y.Attributions()
const attributions = Y.createContentMap()
ydoc.on('update', (update, _origin, doc, tr) => {
if (!tr.local) return
const uid = doc.clientID.toString()
const cids = Y.createContentIdsFromUpdate(update)
Y.insertIntoIdMap(attrs.inserts, Y.createIdMapFromIdSet(cids.inserts, [Y.createContentAttribute('insert', uid)]))
Y.insertIntoIdMap(attrs.deletes, Y.createIdMapFromIdSet(cids.deletes, [Y.createContentAttribute('delete', uid)]))
Y.insertIntoIdMap(attributions.inserts, Y.createIdMapFromIdSet(cids.inserts, [Y.createContentAttribute('insert', uid)]))
Y.insertIntoIdMap(attributions.deletes, Y.createIdMapFromIdSet(cids.deletes, [Y.createContentAttribute('delete', uid)]))
})
root.insert(0, 'hello') // a suggestion relative to v1
const uid = ydoc.clientID.toString()
root.useRenderer(Y.createDiffRenderer(v1, ydoc, { attrs }))
root.useRenderer(Y.createDiffRenderer(v1, ydoc, { attributions }))
t.assert(root.delta.equals(delta.create().insert('hello', null, { insert: [uid] }).done()))
t.assert(root.delta.equals(root.toDelta({ deep: true })))
}
@@ -729,7 +729,7 @@ export const testRdtDeltaAttributionSanity = () => {
export const testRdtFormatAcrossSuggestionDeletedDrift = () => {
const doc = new Y.Doc({ gc: false })
const suggestionDoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attributions: Y.createContentMap() })
doc.get('prosemirror').applyDelta(
delta.create().insert([delta.create('paragraph', {}, 'hello world')]).done()
)
@@ -767,7 +767,7 @@ const createSuggestionPair = (baseClientID, sdocClientID, useRootRenderer = true
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
doc.get('prosemirror').applyDelta(
delta.create().insert([delta.create('paragraph', {}, 'hello world')]).done()
)
@@ -885,7 +885,7 @@ export const testRdtDeltaFreshRangeAfterItemMerge = () => {
doc.clientID = 1
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = 2
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
doc.get('t').applyDelta(delta.create().insert('XY').done())
const ytype = sdoc.get('t')
ytype.useRenderer(renderer)
@@ -1009,7 +1009,7 @@ export const testRdtApplyDeltaNodeFormatOnTombstoneInverse = () => {
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
doc.get('prosemirror').applyDelta(
delta.create().insert([delta.create('paragraph', {}, 'aa'), delta.create('paragraph', {}, 'bb')]).done()
)
@@ -1040,7 +1040,7 @@ export const testRdtApplyDeltaMixedFixCoordinates = () => {
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
doc.get('prosemirror').applyDelta(
delta.create().insert([
delta.create('paragraph', {}, 'aa'), delta.create('paragraph', {}, 'hello world'), delta.create('paragraph', {}, 'cc')
@@ -1079,7 +1079,7 @@ export const testRdtApplyDeltaModifyAttrOnDeletedMapValue = () => {
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
const title = doc.get('m').setAttr('title', new Y.Type())
title.insert(0, 'hi')
const m = sdoc.get('m')
@@ -1152,7 +1152,7 @@ export const testRdtApplyDeltaNestedTombstoneFixBubbles = () => {
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
doc.get('prosemirror').applyDelta(
delta.create().insert([delta.create('paragraph', {}, [delta.create('nested', {}, 'ww')])]).done()
)
@@ -1186,7 +1186,7 @@ export const testRdtApplyDeltaPureDeleteOverTombstoneNoFix = () => {
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
doc.get('prosemirror').applyDelta(
delta.create().insert([
delta.create('paragraph', {}, 'aa'), delta.create('paragraph', {}, 'hello world'), delta.create('paragraph', {}, 'cc')
@@ -1215,7 +1215,7 @@ export const testRdtApplyDeltaDeleteMidStruckChunkKeepsCursorSync = () => {
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
doc.get('t').applyDelta(delta.create().insert('abc').done())
doc.get('t').applyDelta(delta.create().retain(3).insert([delta.create('nA', {}, 'kk'), delta.create('nB', {}, 'qqq')]).done())
const ytype = sdoc.get('t')
@@ -1250,7 +1250,7 @@ export const testRdtDeltaThroughDeletedAttrValue = () => {
doc.clientID = baseClientID
const sdoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
sdoc.clientID = sdocClientID
const renderer = Y.createDiffRenderer(doc, sdoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, sdoc, { attributions: Y.createContentMap() })
const title = doc.get('m').setAttr('title', new Y.Type())
title.insert(0, 'hi')
const m = sdoc.get('m')
@@ -1404,7 +1404,7 @@ export const testRdtDeletedSubtreeUndoScope = () => {
export const testRdtAcceptingNodeInsertCacheDrift = () => {
const doc = new Y.Doc({ gc: false })
const suggestionDoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attributions: Y.createContentMap() })
renderer.suggestionMode = false
doc.get('prosemirror').applyDelta(
delta.create().insert([delta.create('paragraph', {}, 'base para')]).done()
@@ -1429,7 +1429,7 @@ export const testRdtAcceptingNodeInsertCacheDrift = () => {
{
const doc2 = new Y.Doc({ gc: false })
const suggestionDoc2 = new Y.Doc({ isSuggestionDoc: true, gc: false })
const renderer2 = Y.createDiffRenderer(doc2, suggestionDoc2, { attrs: new Y.Attributions() })
const renderer2 = Y.createDiffRenderer(doc2, suggestionDoc2, { attributions: Y.createContentMap() })
doc2.get('prosemirror').applyDelta(delta.create().insert([delta.create('paragraph', {}, 'base para')]).done())
const ytype2 = suggestionDoc2.get('prosemirror')
ytype2.useRenderer(renderer2)
@@ -1458,7 +1458,7 @@ export const testRdtAcceptingNodeInsertCacheDrift = () => {
export const testRdtAcceptingNodeInsertRenderedAsSuggestion = () => {
const doc = new Y.Doc({ gc: false })
const suggestionDoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attributions: Y.createContentMap() })
renderer.suggestionMode = false
doc.get('prosemirror').applyDelta(
delta.create().insert([delta.create('paragraph', {}, 'base para')]).done()
@@ -1499,7 +1499,7 @@ export const testRdtAcceptingNodeInsertRenderedAsSuggestion = () => {
export const testRdtPartialAcceptKeepsPendingChildSuggestions = () => {
const doc = new Y.Doc({ gc: false })
const suggestionDoc = new Y.Doc({ isSuggestionDoc: true, gc: false })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(doc, suggestionDoc, { attributions: Y.createContentMap() })
doc.get('prosemirror').applyDelta(delta.create().insert([delta.create('paragraph', {}, 'base para')]).done())
const ytype = suggestionDoc.get('prosemirror')
ytype.useRenderer(renderer)
@@ -1553,7 +1553,7 @@ export const testRdtBaseInsertIntoOverlappingFormatRangeCacheDrift = () => {
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1599,7 +1599,7 @@ export const testRdtSuggestedInsertUnderTwoKeySuggestedFormatsCacheDrift = () =>
base.clientID = 1
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 0
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1646,7 +1646,7 @@ export const testRdtFormattedBaseInsertUnderTwoKeySuggestedFormatsCacheDrift = (
base.clientID = 1
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 0
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1698,7 +1698,7 @@ export const testRdtBaseFormatEndingInsideSuggestedFormatRangeFormatValueCacheDr
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1748,7 +1748,7 @@ export const testRdtSuggestedRemovalOfOverriddenBaseFormatCacheDrift = () => {
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1797,7 +1797,7 @@ export const testRdtStagedSuggestedUnformatOfBaseFormatCacheDrift = () => {
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1850,7 +1850,7 @@ export const testRdtAcceptAllOfSameValueSuggestedFormatCacheDrift = () => {
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1901,7 +1901,7 @@ export const testRdtRejectAllOverlappingSameKeyFormatProvenanceResidual = () =>
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -1963,7 +1963,7 @@ export const testRdtSplitFormatMarkerPairPartialAcceptThenUnformatCacheDrift = (
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -2025,7 +2025,7 @@ export const testRdtPartialAcceptSkippingUnformatOpenMarkerCacheDrift = () => {
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -2093,7 +2093,7 @@ export const testRdtBaseUnformatInsideBlockDeletedParagraphCacheDrift = () => {
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')
@@ -2149,7 +2149,7 @@ export const testRdtBaseFormatClearInsideSuggestionDeletedParagraphCacheDrift =
base.clientID = 0
const sugg = new Y.Doc({ isSuggestionDoc: true, gc: false })
sugg.clientID = 1
const renderer = Y.createDiffRenderer(base, sugg, { attrs: new Y.Attributions() })
const renderer = Y.createDiffRenderer(base, sugg, { attributions: Y.createContentMap() })
renderer.suggestionMode = true
const bt = base.get('t')
const st = sugg.get('t')

View File

@@ -467,11 +467,11 @@ export const testAttributedContent = _tc => {
*/
const yarray = ydoc.get()
yarray.insert(0, [1, 2])
let renderer = /** @type {AbstractRenderer?} */ (Y.baseRenderer)
let renderer = /** @type {AbstractRenderer?} */ (null)
ydoc.on('afterTransaction', tr => {
// renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new Y.TwosetRenderer(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, []))
// renderer = new AttributionsRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new Y.AttributionsRenderer(Y.createContentMap(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, [])))
})
t.group('insert / delete', () => {
ydoc.transact(() => {

View File

@@ -1,6 +1,6 @@
import * as Y from '../src/index.js'
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
import { baseRenderer, TwosetRenderer } from '../src/utils/Renderer.js'
import { AttributionsRenderer } from '../src/utils/Renderer.js'
import { createIdMapFromIdSet } from '../src/utils/ids.js'
import * as t from 'lib0/testing'
import * as prng from 'lib0/prng'
@@ -552,11 +552,11 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
export const testAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const ymap = ydoc.get()
let renderer = /** @type {AbstractRenderer?} */ (baseRenderer)
let renderer = /** @type {AbstractRenderer?} */ (null)
ydoc.on('afterTransaction', tr => {
// renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, []))
// renderer = new AttributionsRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new AttributionsRenderer(Y.createContentMap(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])))
})
t.group('initial value', () => {
ymap.setAttr('test', 42)

View File

@@ -4,7 +4,7 @@ import * as prng from 'lib0/prng'
import * as math from 'lib0/math'
import * as delta from 'lib0/delta'
import { createIdMapFromIdSet } from '../src/utils/ids.js'
import { baseRenderer, TwosetRenderer, createSnapshotRenderer } from '../src/utils/Renderer.js'
import { AttributionsRenderer, createSnapshotRenderer } from '../src/utils/Renderer.js'
const { init, compare } = Y
@@ -1900,11 +1900,11 @@ export const testAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const ytext = ydoc.get()
ytext.insert(0, 'Hello World!')
let renderer = /** @type {AbstractRenderer?} */ (baseRenderer)
let renderer = /** @type {AbstractRenderer?} */ (null)
ydoc.on('afterTransaction', tr => {
// renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, []))
// renderer = new AttributionsRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new AttributionsRenderer(Y.createContentMap(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])))
})
t.group('insert / delete / format', () => {
ytext.applyDelta(delta.create().retain(4, { italic: true }).retain(2).delete(5).insert('attributions').done())
@@ -1944,8 +1944,8 @@ export const testAttributedDiffing = _tc => {
const attributedInsertions = createIdMapFromIdSet(insertionSetDiff, [Y.createContentAttribute('insert', 'Bob')])
const attributedDeletions = createIdMapFromIdSet(deleteSetDiff, [Y.createContentAttribute('delete', 'Bob')])
// now we can define an attribution manager that maps these changes to output. One of the
// implementations is the TwosetRenderer
const renderer = new TwosetRenderer(attributedInsertions, attributedDeletions)
// implementations is the AttributionsRenderer
const renderer = new AttributionsRenderer(Y.createContentMap(attributedInsertions, attributedDeletions))
// we render the attributed content with the renderer
const attributedContent = ytext.toDelta({ renderer })
console.log(JSON.stringify(attributedContent.toJSON(), null, 2))
@@ -1954,6 +1954,52 @@ export const testAttributedDiffing = _tc => {
console.log(Y.encodeIdMap(attributedInsertions).length)
}
/**
* The `renderedContent` option decouples "what is visible" from the doc's deleted state and from
* attributions: content in `renderedContent` renders normally (even if deleted → "restore"), alive
* content omitted from it is hidden, and attributions always render (even outside it).
*
* @param {t.TestCase} _tc
*/
export const testAttributionsRendererRenderedContent = _tc => {
const ydoc = new Y.Doc({ gc: false }) // restore requires gc:false (no rehydration of gc'd content)
ydoc.clientID = 1
const ytext = ydoc.get()
ytext.insert(0, 'Hello World!')
ytext.delete(5, 6) // delete ' World' (clock 5..10) → the visible doc is 'Hello!'
t.compare(ytext.toDelta().toJSON(), delta.create().insert('Hello!').done().toJSON()) // sanity: default hides the deletion
const emptyAttributions = Y.createContentMap()
const aliveSet = Y.createInsertSetFromStructStore(ydoc.store, true) // {0..4, 11}
t.group('restore: deleted content in renderedContent renders normally', () => {
const renderedContent = Y.createInsertSetFromStructStore(ydoc.store, false) // all content incl. deleted
const renderer = new AttributionsRenderer(emptyAttributions, { renderedContent })
const expected = delta.create().insert('Hello World!')
const rendered = ytext.toDelta({ renderer })
t.assert(rendered.equals(expected))
})
t.group('hide: alive content omitted from renderedContent renders as nothing', () => {
const hidden = Y.createIdSet()
hidden.add(1, 1, 4) // hide 'ello' (clock 1..4)
const renderer = new AttributionsRenderer(emptyAttributions, { renderedContent: Y.diffIdSet(aliveSet, hidden) })
const expected = delta.create().insert('H!')
const rendered = ytext.toDelta({ renderer })
t.assert(rendered.equals(expected))
})
t.group('attributions always render, even outside renderedContent', () => {
const deletedRange = Y.createIdSet()
deletedRange.add(1, 5, 6) // ' World'
const attributions = Y.createContentMap(Y.createIdMap(), createIdMapFromIdSet(deletedRange, [Y.createContentAttribute('delete', 'bob')]))
const renderer = new AttributionsRenderer(attributions, { renderedContent: aliveSet })
const expected = delta.create().insert('Hello').insert(' World', {}, { delete: ['bob'] }).insert('!')
const rendered = ytext.toDelta({ renderer })
t.assert(rendered.equals(expected))
})
}
// RANDOM TESTS
let charCounter = 0
@@ -2219,6 +2265,54 @@ export const testRendererDefaultPerformance = tc => {
})
}
/**
* Benchmark the generic fast path (default / no renderer) against an {@link AttributionsRenderer}
* configured with *no* attributions (an empty {@link ContentMap}) and a custom `renderedContent`
* set to the doc's full insert set. Since the insert set also contains the ids of deleted content,
* the renderer *restores* every deletion — rendering the whole document normally, without
* attributions — while alive content (fully covered by `renderedContent`) still takes the generic
* fast path. This measures the renderer's restore path (`hasItem`'s restore branch, `readContent`'s
* custom-`renderedContent` path, and `contentLength` via `coveredLength`) against the default.
*
* @param {t.TestCase} tc
*/
export const testRendererAttributionsPerformance = tc => {
const N = 10000
const MaxDeletionLength = 5 // 25% chance of deletion
const MaxInsertionLength = 5
const ydoc = new Y.Doc({ gc: false }) // keep deleted content so the delete set renders real content
const ytext = ydoc.get()
for (let i = 0; i < N; i++) {
if (prng.bool(tc.prng) && prng.bool(tc.prng) && ytext.length > 0) {
const index = prng.int31(tc.prng, 0, ytext.length - 1)
const len = prng.int31(tc.prng, 0, math.min(ytext.length - index, MaxDeletionLength))
ytext.delete(index, len)
} else {
const index = prng.int31(tc.prng, 0, ytext.length)
const content = prng.utf16String(tc.prng, MaxInsertionLength)
ytext.insert(index, content)
}
}
// no attributions; renderedContent = the full insert set, which also covers deleted ids, so every
// deletion is restored and rendered normally
const insertSet = Y.createInsertSetFromStructStore(ydoc.store, false)
// const deleteSet = Y.createDeleteSetFromStructStore(ydoc.store)
const renderer = new AttributionsRenderer(Y.createContentMap(), { renderedContent: insertSet })
t.info(`number of changes: ${N / 1000}k`)
t.info(`length of visible text: ${ytext.length}`)
const M = 10
t.measureTime(`default renderer (fast path): toDelta() <executed ${M} times>`, () => {
for (let i = 0; i < M; i++) {
ytext.toDelta()
}
})
t.measureTime(`AttributionsRenderer (no attributions, renderedContent = insert set): toDelta({ renderer }) <executed ${M} times>`, () => {
for (let i = 0; i < M; i++) {
ytext.toDelta({ renderer })
}
})
}
/**
* @param {t.TestCase} tc
*/

View File

@@ -139,10 +139,10 @@ export const testFragmentAttributedContent = _tc => {
const elem3 = Y.Type.from(delta.create().insert('world'))
yfragment.insert(0, [elem1, elem2])
ydoc.get().insert(0, [yfragment])
let renderer = /** @type {AbstractRenderer?} */ (Y.baseRenderer)
let renderer = /** @type {AbstractRenderer?} */ (null)
ydoc.on('afterTransaction', tr => {
// renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new Y.TwosetRenderer(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, []))
// renderer = new AttributionsRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new Y.AttributionsRenderer(Y.createContentMap(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, [])))
})
t.group('insert / delete', () => {
ydoc.transact(() => {
@@ -167,10 +167,10 @@ export const testElementAttributedContent = _tc => {
const elem2 = delta.create('span').done()
const elem3 = delta.create().insert('world').done()
yelement.insert(0, [elem1, elem2])
let renderer = /** @type {AbstractRenderer?} */ (Y.baseRenderer)
let renderer = /** @type {AbstractRenderer?} */ (null)
ydoc.on('afterTransaction', tr => {
// renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new Y.TwosetRenderer(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, []))
// renderer = new AttributionsRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
renderer = new Y.AttributionsRenderer(Y.createContentMap(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, [])))
})
t.group('insert / delete', () => {
ydoc.transact(() => {