import * as Y from '../src/index.js' import * as t from 'lib0/testing' import * as dpos from 'lib0/delta/position' /** * @param {Y.Node<{text:true}>} ytext */ const checkRelativePositions = ytext => { // test if all positions are encoded and restored correctly for (let i = 0; i < ytext.length; i++) { // for all types of associations.. for (let assoc = -1; assoc < 2; assoc++) { const rpos = Y.createRelativePositionFromTypeIndex(ytext, i, assoc) const encodedRpos = Y.encodeRelativePosition(rpos) const decodedRpos = Y.decodeRelativePosition(encodedRpos) const absPos = /** @type {Y.AbsolutePosition} */ (Y.createAbsolutePositionFromRelativePosition(decodedRpos, /** @type {Y.Doc} */ (ytext.doc))) t.assert(absPos.index === i) t.assert(absPos.assoc === assoc) } } } /** * @param {t.TestCase} _tc */ export const testRelativePositionCase1 = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, '1') ytext.insert(0, 'abc') ytext.insert(0, 'z') ytext.insert(0, 'y') ytext.insert(0, 'x') checkRelativePositions(ytext) } /** * @param {t.TestCase} _tc */ export const testRelativePositionCase2 = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, 'abc') checkRelativePositions(ytext) } /** * @param {t.TestCase} _tc */ export const testRelativePositionCase3 = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, 'abc') ytext.insert(0, '1') ytext.insert(0, 'xyz') checkRelativePositions(ytext) } /** * @param {t.TestCase} _tc */ export const testRelativePositionCase4 = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, '1') checkRelativePositions(ytext) } /** * @param {t.TestCase} _tc */ export const testRelativePositionCase5 = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, '2') ytext.insert(0, '1') checkRelativePositions(ytext) } /** * @param {t.TestCase} _tc */ export const testRelativePositionCase6 = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() checkRelativePositions(ytext) } /** * Testing https://github.com/yjs/yjs/issues/657 * * @param {t.TestCase} _tc */ export const testRelativePositionCase7 = _tc => { const docA = new Y.Doc() const textA = docA.get('text') textA.insert(0, 'abcde') // Create a relative position at index 2 in 'textA' const relativePosition = Y.createRelativePositionFromTypeIndex(textA, 2) // Verify that the absolutes positions on 'docA' are the same const absolutePositionWithFollow = Y.createAbsolutePositionFromRelativePosition(relativePosition, docA, true) const absolutePositionWithoutFollow = Y.createAbsolutePositionFromRelativePosition(relativePosition, docA, false) t.assert(absolutePositionWithFollow?.index === 2) t.assert(absolutePositionWithoutFollow?.index === 2) } /** * @param {t.TestCase} _tc */ export const testRelativePositionAssociationDifference = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, '2') ytext.insert(0, '1') const rposRight = Y.createRelativePositionFromTypeIndex(ytext, 1, 0) const rposLeft = Y.createRelativePositionFromTypeIndex(ytext, 1, -1) ytext.insert(1, 'x') const posRight = Y.createAbsolutePositionFromRelativePosition(rposRight, ydoc) const posLeft = Y.createAbsolutePositionFromRelativePosition(rposLeft, ydoc) t.assert(posRight != null && posRight.index === 2) t.assert(posLeft != null && posLeft.index === 1) } /** * @param {t.TestCase} _tc */ export const testRelativePositionWithUndo = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, 'hello world') const rpos = Y.createRelativePositionFromTypeIndex(ytext, 1) const um = new Y.UndoManager(ytext) ytext.delete(0, 6) t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 0) um.undo() t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 1) const posWithoutFollow = Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false) console.log({ posWithoutFollow }) t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)?.index === 6) const ydocClone = new Y.Doc() Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc)) t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone)?.index === 6) t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone, false)?.index === 6) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsFlatTextRoundtrip = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, 'hello world') const positions = [dpos.create([0]), dpos.create([5]), dpos.create([5], -1), dpos.create([11])] const rposs = Y.createRelativePositionsFromDeltaPositions(ytext, positions) t.assert(rposs.every(rpos => rpos !== null)) const back = Y.createDeltaPositionsFromRelativePositions(ytext, rposs) positions.forEach((pos, i) => { const b = back[i] t.assert(b !== null && dpos.equals(b, pos)) }) // the singular wrappers behave like a one-element batch const rpos = Y.createRelativePositionFromDeltaPosition(ytext, dpos.create([5])) t.assert(Y.compareRelativePositions(rpos, rposs[1])) const single = Y.createDeltaPositionFromRelativePosition(ytext, rpos) t.assert(single !== null && dpos.equals(single, positions[1])) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsNested = _tc => { const ydoc = new Y.Doc() const yroot = ydoc.get('arr') yroot.insert(0, ['a', new Y.Node(), 'b']) const child = /** @type {Y.Node} */ (yroot.get(1)) child.insert(0, 'xyz') const positions = [dpos.create([1, 2]), dpos.create([0])] const rposs = Y.createRelativePositionsFromDeltaPositions(yroot, positions) t.assert(rposs.every(rpos => rpos !== null)) const back = Y.createDeltaPositionsFromRelativePositions(yroot, rposs) positions.forEach((pos, i) => { const b = back[i] t.assert(b !== null && dpos.equals(b, pos)) }) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsAttrDescent = _tc => { const ydoc = new Y.Doc() const ymap = ydoc.get('map') const child = ymap.setAttr('k', new Y.Node()) child.insert(0, 'ab') const pos = dpos.create(['k', 1]) const rpos = Y.createRelativePositionFromDeltaPosition(ymap, pos) t.assert(rpos !== null) const back = Y.createDeltaPositionFromRelativePosition(ymap, rpos) t.assert(back !== null && dpos.equals(back, pos)) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsAttrLeaf = _tc => { const ydoc = new Y.Doc() const ymap = ydoc.get('map') ymap.setAttr('k', 42) const pos = dpos.create(['k']) const rpos = Y.createRelativePositionFromDeltaPosition(ymap, pos) t.assert(rpos !== null && rpos.item !== null) t.assert(dpos.equals(/** @type {dpos.Pos} */ (Y.createDeltaPositionFromRelativePosition(ymap, rpos)), pos)) // the attribute-leaf anchor survives overwriting the attribute ymap.setAttr('k', 43) t.assert(dpos.equals(/** @type {dpos.Pos} */ (Y.createDeltaPositionFromRelativePosition(ymap, rpos)), pos)) // .. but not deleting it ymap.deleteAttr('k') t.assert(Y.createDeltaPositionFromRelativePosition(ymap, rpos) === null) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsDocRooted = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get('text') ytext.insert(0, 'hello') // doc-rooted paths start with a root-type name; numeric or terminal-string steps at doc level are unresolvable const positions = [dpos.create(['text', 3]), dpos.create([0]), dpos.create(['text'])] const rposs = Y.createRelativePositionsFromDeltaPositions(ydoc, positions) t.assert(rposs[0] !== null && rposs[1] === null && rposs[2] === null) const back = Y.createDeltaPositionFromRelativePosition(ydoc, rposs[0]) t.assert(back !== null && dpos.equals(back, positions[0])) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsAssocThroughEdit = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, '2') ytext.insert(0, '1') const rposs = Y.createRelativePositionsFromDeltaPositions(ytext, [dpos.create([1]), dpos.create([1], -1)]) ytext.insert(1, 'x') const back = Y.createDeltaPositionsFromRelativePositions(ytext, rposs) t.assert(back[0] !== null && dpos.equals(back[0], dpos.create([2]))) t.assert(back[1] !== null && dpos.equals(back[1], dpos.create([1], -1))) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsSurviveConcurrentEdit = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get('text') ytext.insert(0, 'hello') const rpos = Y.createRelativePositionFromDeltaPosition(ytext, dpos.create([2])) t.assert(rpos !== null) const encoded = Y.encodeRelativePosition(/** @type {Y.RelativePosition} */ (rpos)) const ydoc2 = new Y.Doc() Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc)) const ytext2 = ydoc2.get('text') ytext2.insert(0, 'abc') const back = Y.createDeltaPositionFromRelativePosition(ytext2, Y.decodeRelativePosition(encoded)) t.assert(back !== null && dpos.equals(back, dpos.create([5]))) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsWithRenderer = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, 'hello world') const v1 = Y.cloneDoc(ydoc) ytext.delete(1, 6) ytext.insert(1, 'x') const renderer = Y.createDiffRenderer(v1, ydoc) const pos = dpos.create([9]) // pos after "hello wo" in the rendered diff const rpos = Y.createRelativePositionFromDeltaPosition(ytext, pos, { renderer }) t.assert(rpos !== null) const backWith = Y.createDeltaPositionFromRelativePosition(ytext, rpos, { renderer }) t.assert(backWith !== null && dpos.equals(backWith, pos)) const backWithout = Y.createDeltaPositionFromRelativePosition(ytext, rpos) t.assert(backWithout !== null && dpos.equals(backWithout, dpos.create([3]))) // the node's own renderer is the default - matching toDelta / node.delta ytext.useRenderer(renderer) const rpos2 = Y.createRelativePositionFromDeltaPosition(ytext, pos) t.assert(Y.compareRelativePositions(rpos, rpos2)) const backDefault = Y.createDeltaPositionFromRelativePosition(ytext, rpos2) t.assert(backDefault !== null && dpos.equals(backDefault, pos)) // explicit renderer: null overrides the node's renderer const backNull = Y.createDeltaPositionFromRelativePosition(ytext, rpos2, { renderer: null }) t.assert(backNull !== null && dpos.equals(backNull, dpos.create([3]))) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsUnresolvable = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, 'abc') const positions = [ dpos.create([0, 0]), // descend into a scalar dpos.create(['missing', 0]), // missing attribute dpos.create([]), // a node reference doesn't round-trip dpos.create([10, 0]), // out-of-range non-terminal index dpos.create([1]) // resolvable - other entries must not affect it ] const rposs = Y.createRelativePositionsFromDeltaPositions(ytext, positions) t.assert(rposs[0] === null && rposs[1] === null && rposs[2] === null && rposs[3] === null) t.assert(rposs[4] !== null) const back = Y.createDeltaPositionsFromRelativePositions(ytext, rposs) t.assert(back[0] === null && back[1] === null && back[2] === null && back[3] === null) t.assert(back[4] !== null && dpos.equals(back[4], positions[4])) } /** * @param {t.TestCase} _tc */ export const testDeltaPositionsEndOfNode = _tc => { const ydoc = new Y.Doc() const ytext = ydoc.get() ytext.insert(0, 'hello') const rpos = Y.createRelativePositionFromDeltaPosition(ytext, dpos.create([5])) t.assert(rpos !== null && rpos.item === null) t.assert(dpos.equals(/** @type {dpos.Pos} */ (Y.createDeltaPositionFromRelativePosition(ytext, rpos)), dpos.create([5]))) // an end-of-node position sticks to the end ytext.insert(5, ' world') t.assert(dpos.equals(/** @type {dpos.Pos} */ (Y.createDeltaPositionFromRelativePosition(ytext, rpos)), dpos.create([11]))) // empty node const yempty = ydoc.get('empty') const rposE = Y.createRelativePositionFromDeltaPosition(yempty, dpos.create([0])) t.assert(rposE !== null && rposE.item === null) t.assert(dpos.equals(/** @type {dpos.Pos} */ (Y.createDeltaPositionFromRelativePosition(yempty, rposE)), dpos.create([0]))) } /** * Positions inside a nested node that is deleted but still rendered (e.g. a suggestion-mode * subtree delete). Requires gc: false - with gc enabled, the deleted subtree's content is * destroyed and such positions are unresolvable. * * @param {t.TestCase} _tc */ export const testDeltaPositionsInDeletedRenderedSubtree = _tc => { const ydoc = new Y.Doc({ gc: false }) const yroot = ydoc.get('r') yroot.insert(0, [new Y.Node(), 'x']) const mid = /** @type {Y.Node} */ (yroot.get(0)) mid.insert(0, ['q', new Y.Node()]) const leaf = /** @type {Y.Node} */ (mid.get(1)) leaf.insert(0, 'hello') const v1 = Y.cloneDoc(ydoc) yroot.delete(0, 1) // delete the whole `mid` subtree - the diff renderer still renders it const renderer = Y.createDiffRenderer(v1, ydoc) // the underlying primitive resolves indexes inside the deleted-but-rendered type const rposLeaf = Y.createRelativePositionFromTypeIndex(leaf, 3, 0, renderer) t.assert(Y.createAbsolutePositionFromRelativePosition(rposLeaf, ydoc, true, renderer)?.index === 3) // .. and delta positions round-trip through the deleted subtree const positions = [dpos.create([0, 1, 3]), dpos.create([0, 0]), dpos.create([1])] const rposs = Y.createRelativePositionsFromDeltaPositions(yroot, positions, { renderer }) t.assert(rposs.every(rpos => rpos !== null)) const back = Y.createDeltaPositionsFromRelativePositions(yroot, rposs, { renderer }) positions.forEach((pos, i) => { const b = back[i] t.assert(b !== null && dpos.equals(b, pos)) }) }