diff --git a/src/utils/RelativePosition.js b/src/utils/RelativePosition.js index a1b4356b..744dff37 100644 --- a/src/utils/RelativePosition.js +++ b/src/utils/RelativePosition.js @@ -8,6 +8,7 @@ import { createID, ContentType, followRedone, + getItem, ID, Doc, AbstractType // eslint-disable-line } from '../internals.js' @@ -256,13 +257,24 @@ export const readRelativePosition = decoder => { export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array)) /** + * Transform a relative position to an absolute position. + * + * If you want to share the relative position with other users, you should set + * `followUndoneDeletions` to false to get consistent results across all clients. + * + * When calculating the absolute position, we try to follow the "undone deletions". This yields + * better results for the user who performed undo. However, only the user who performed the undo + * will get the better results, the other users don't know which operations recreated a deleted + * range of content. There is more information in this ticket: https://github.com/yjs/yjs/issues/638 + * * @param {RelativePosition} rpos * @param {Doc} doc + * @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638 * @return {AbsolutePosition|null} * * @function */ -export const createAbsolutePositionFromRelativePosition = (rpos, doc) => { +export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => { const store = doc.store const rightID = rpos.item const typeID = rpos.type @@ -274,7 +286,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => { if (getState(store, rightID.client) <= rightID.clock) { return null } - const res = followRedone(store, rightID) + const res = followUndoneDeletions ? followRedone(store, rightID) : { item: getItem(store, rightID), diff: 0 } const right = res.item if (!(right instanceof Item)) { return null @@ -298,7 +310,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => { // type does not exist yet return null } - const { item } = followRedone(store, typeID) + const { item } = followUndoneDeletions ? followRedone(store, typeID) : { item: getItem(store, typeID) } if (item instanceof Item && item.content instanceof ContentType) { type = item.content.type } else { diff --git a/tests/relativePositions.tests.js b/tests/relativePositions.tests.js index 93fec230..ab86168b 100644 --- a/tests/relativePositions.tests.js +++ b/tests/relativePositions.tests.js @@ -101,3 +101,25 @@ export const testRelativePositionAssociationDifference = tc => { 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.getText() + 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) +}