fix: rename colliding keys before backspace merges a block into its predecessor - #3179
fix: rename colliding keys before backspace merges a block into its predecessor#3179christianhg wants to merge 2 commits into
Conversation
…redecessor Backspace at the start of a text block raises `delete.block` + `insert.block`, and `adjustFragmentKeys` silently minted fresh keys for any child or markDef colliding with the destination block, so the wire read destroy-and-create and receivers could follow nothing through the merge. The boundary-merge behavior now plans the renames first: `set` patches on keyed `_key` paths (markDef renames rewrite the referencing `marks`), raised while the merging block is still addressable, and the captured node passed to `insert.block` carries the renamed keys, so no collision handling fires downstream. The wire becomes rename, unset, reinsert under final keys; non-colliding merges emit byte-identical patches to before. On the receiving side, `interpretTransaction` records each renamed node's path in `renamedKeyPaths` at rename time and registers exactly those descendants of a whole-block `unset` as move candidates, identity-matched by path, never by key value (a rename shadowing a pre-existing twin key pairs nothing). The container's `remove.node` survives descendant moves, relocated after them, so never-renamed siblings invalidate instead of dangling. `rekey` steps carry the renamed node's container path and the mapper rewrites only the segment at that exact location, keeping destination-block carets on twin keys untouched. Pinned by the `block-merge-duplicate-keys` wire-catalogue row, interpreter probes (shadowed rename, partial collision, chained rename, rename-then-container-hop, destination-caret stability), and two-editor browser tests for emission shape, caret follow-through, and markDef rename rewrites.
The auto-generated inverse for a `set` on a node's own `_key` addressed the node by its pre-rename keyed path, which stops resolving the moment the rename applies, so undo silently dropped the rename while reverting everything else in the step. Both inverse constructions (`setNodeProperties` and the engine's `set` apply path) re-target the post-rename key. Redo is unaffected: it replays the original operations, whose pre-rename paths resolve again after undo. Pinned by a merge-undo-redo-undo round-trip over the colliding-keys merge, asserting the exact full value, every `_key` included, at each stop.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: e03a169 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Bundle Stats✅ No significant changes. All scenario measurements (7)🗺️
Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time. |
Stacked on #3154 (it supplies the transaction interpreter this change extends).
Merging a block into its predecessor with backspace re-keys any child span or annotation whose
_keycollides with one in the destination block. Today those fresh keys are minted silently before insertion, so the wire reads "block deleted, unrelated spans created": a receiving editor can connect nothing, and a collaborator whose caret sat in the merged-away block loses it (reproduced with two editors: the published selection ends up pointing into the removed block).The fix makes the merge say what it did. Before deleting the merging block, the boundary-merge behavior renames the colliding keys in place,
setpatches on keyed_keypaths, rewritingmarksreferences when a markDef is renamed, and inserts the children under their final keys. Rename-first matters: renaming after insertion would put twin keys among siblings. Non-colliding merges emit byte-identical patches to before.The interpreter follows the new shape by identity, never by key value: renamed nodes are tracked by their recorded path, a rename shadowing a pre-existing twin key pairs nothing, and
rekeysteps are path-scoped so a destination-block caret sitting on a twin key stays put. Adversarial probes pin each of those refusals.One fix rides along in its own commit: undoing any
_keyrename silently dropped the rename, because the auto-generated inverse addressed the node by a path that stopped resolving once the rename applied. Inverses now target the post-rename key, pinned by a merge-undo-redo-undo round-trip.