Skip to content

fix: dedupe identical same-key markDefs on every block merge - #3195

Draft
christianhg wants to merge 2 commits into
mainfrom
identical-markdefs-non-conflicts
Draft

fix: dedupe identical same-key markDefs on every block merge#3195
christianhg wants to merge 2 commits into
mainfrom
identical-markdefs-non-conflicts

Conversation

@christianhg

Copy link
Copy Markdown
Member

An annotation's _key is its identity: comments and decorations follow it, and receivers pair patches through it. Two paths treated a byte-identical markDef under the same _key as a conflict and renamed it, churning that identity for content that is the same annotation: insert.block's fragment adjustment (which also duplicated the def through its plain markDefs append), and, forced by it, the collapsed backspace/forward-delete merges. The range-delete merge already deduped (#3187), shipping the asymmetry behind a dedupeEqualMarkDefs flag.

The first commit fixes the root: adjustFragmentKeys compares colliding defs with isDeepEqual; an identical def keeps its key, is excluded from the appended defs, and its spans' marks resolve to the destination's copy. Different-content defs rename as before, pinned by a contrast test that fails when the equality branch is forced. The second commit deletes the flag: the planner always keeps an identical def unrenamed and inside the merging block (it must stay there, since insert.block parses blocks standalone and strips marks that don't resolve in the block's own markDefs), reporting it so mergeBlock skips the append.

The user-visible delta beyond the rename churn: splitting an annotated block and merging it back now rejoins the annotation into one def under its original key. The gherkin scenario that pinned the old duplicate-def outcome under a "possible wrong behaviour" warning now pins the rejoin. Undo of a deduped merge restores both original blocks byte-identically, pinned by an undo-redo-undo round-trip; wire-catalogue fixtures are untouched.

…g text block fragments

`adjustFragmentKeys` renamed a fragment markDef whenever its `_key`
existed on the destination block, on key membership alone. A def that
is byte-identical to the destination's is the same annotation, so the
rename churned identity for nothing: consumers that track annotations
by `_key` (comments, decorations) lost the annotation they were
following, and the caller's plain markDefs append then stored the same
content twice under two keys.

Colliding defs are now compared with `isDeepEqual`: an identical def
keeps its key, is excluded from the defs appended into the
destination, and its spans' `marks` stay untouched, so they resolve
to the destination's copy. Same-key defs with different content rename
exactly as before, pinned by a contrast test that fails when the
equality branch is forced.

One rider: `isDeepEqual` gains an explicit `boolean` return type.
The inferred type predicate narrowed the non-equal branch of same-type
comparisons to `never`, which broke compilation at the new call
site.
`planMergeKeyRenames` carried a required `dedupeEqualMarkDefs`
flag: the range-delete merge deduped identical same-key defs, but the
collapsed backspace/forward-delete merges had to keep renaming them,
because their `renamedBlock` feeds `insert.block`, where
`parseBlock` parses the block standalone and strips any mark that
does not resolve in the block's own `markDefs`: a deduped def's
spans would have lost the annotation entirely.

With `insert.block`'s fragment adjustment now treating identical
defs as non-conflicts, that constraint is gone and the flag deletes.
The planner always keeps an identical same-key def unrenamed and
inside `renamedBlock.markDefs` (the block must carry the def so
`parseBlock` resolves its marks) and reports it in
`dedupedMarkDefKeys`; `mergeBlock` skips appending those keys.

Net behavior change: the collapsed merges now dedupe. Splitting an
annotated block and merging it back rejoins the annotation into one
def under its original key, where it previously duplicated the def and
re-keyed half the spans; the `annotations-across-blocks` gherkin
scenario that pinned the old outcome under a 'possible wrong
behaviour' warning now pins the rejoin, and the `unique-sibling-keys`
pin of the same shape follows. Undo of a deduped merge restores both
original blocks byte-identically, pinned by an undo-redo-undo
round-trip. The range-delete path's behavior is unchanged.
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview Aug 27, 2026 3:02pm
portable-text-example-basic Ready Ready Preview Aug 27, 2026 3:02pm
portable-text-playground Ready Ready Preview Aug 27, 2026 3:02pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cad0f5e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@portabletext/editor Patch
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-dnd Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-list-index Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-table Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

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

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Bundle Stats

✅ No significant changes.

All scenario measurements (7)

🗺️ @portabletext/editor / @portabletext/editor · @portabletext/editor / @portabletext/editor/behaviors · @portabletext/editor / @portabletext/editor/plugins · @portabletext/editor / @portabletext/editor/selectors · @portabletext/editor / @portabletext/editor/traversal · @portabletext/editor / @portabletext/editor/utils · @portabletext/markdown / @portabletext/markdown · Artifacts

Scenario Kind Bundle (raw / gzip) Gzip change Import time Import change
⚪ @portabletext/editor / @portabletext/editor export 1.09 MB / 253.5 KB +343 B, +0.1% 65 ms +1 ms, +0.9%
⚪ @portabletext/editor / @portabletext/editor/behaviors export 4.0 KB / 1.4 KB None 2 ms +0 ms, +1.0%
⚪ @portabletext/editor / @portabletext/editor/plugins export 5.1 KB / 1.8 KB None 7 ms +0 ms, +0.6%
⚪ @portabletext/editor / @portabletext/editor/selectors export 93.4 KB / 21.3 KB None 8 ms +0 ms, +4.0%
⚪ @portabletext/editor / @portabletext/editor/traversal export 41.4 KB / 10.7 KB None 6 ms +0 ms, +3.5%
⚪ @portabletext/editor / @portabletext/editor/utils export 32.8 KB / 8.6 KB None 5 ms -0 ms, -2.0%
⚪ @portabletext/markdown / @portabletext/markdown export 272.2 KB / 79.6 KB None 35 ms -1 ms, -2.9%

Significant means at least 1.0 KB and 1% gzip, or at least 5 ms and 10% import time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant