mirror of
https://github.com/yjs/yjs.git
synced 2026-09-02 03:58:33 +02:00
renamed TwoSetRenderer => AttributionsRenderer, AttributionsRenderer refactor
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user