docs/solutions/logic-errors/2026-05-14-slate-bookmarks-replace-children-should-follow-surviving-text.md
Persistent annotation anchors failed after a fragment insert before the anchored
text. The document still contained the logical text, but the bookmark backing
the annotation resolved to null, so the React annotation store projected
none.
persistent-annotation-anchors expected comment-anchor:8-11, but rendered
none.1.0:8|1.0:11,
but got none.Cannot project a range outside the committed snapshot because replacement paths were rebased incorrectly.RangeApi.transform(..., replace_children) was not enough. It must
fail closed for ordinary refs inside a replaced child window, but bookmarks
are durable anchors and can preserve more intent.op.path.concat(op.index) was wrong for
child-list replacement. It produced paths like [0,1,0] instead of rebasing
the replacement-window child index to [1,0].Keep generic point/range refs conservative, but give bookmarks a
replace_children transform that:
newChildren,The core regression should use the public editor path, not React:
const bookmark = Editor.bookmark(
editor,
createRange({ path: [0, 0], offset: 1 }, { path: [0, 0], offset: 4 })
)
editor.update((tx) => {
tx.selection.set({
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 0 },
})
tx.fragment.insert([
{ type: 'paragraph', children: [{ text: 'intro-a' }] },
{ type: 'paragraph', children: [{ text: 'intro-b' }] },
])
})
assert.deepEqual(bookmark.resolve(), {
anchor: { path: [1, 0], offset: 8 },
focus: { path: [1, 0], offset: 11 },
})
Keep the React annotation-store test as the integration proof that refreshed projections can see the rebased bookmark after root runtime ids change.
replace_children is intentionally broad: it can represent a paste,
canonical remote reconcile, or fragment replacement. Ordinary refs inside the
replaced window cannot assume semantic continuity, so they should still null.
Bookmarks are different. They are durable annotation-style anchors. If the old text leaf survives uniquely inside the replacement, the bookmark can follow that text. If the replacement is a canonical same-position swap, the bookmark can preserve the same relative path and offset. If neither condition is true, failing closed remains the correct behavior.
replace_children, keep ordinary refs conservative and make bookmark
behavior explicit. Durable anchors and normal refs do not have the same
contract.op.path.concat(op.index + relativeChildIndex, childPath).