Skip to content

feat(versioning): diff nesting changes in place - #3168

Draft
YousefED wants to merge 9 commits into
fix/legacy-version-diff-structurefrom
feat/nesting-diffs-in-place
Draft

YousefED wants to merge 9 commits into
fix/legacy-version-diff-structurefrom
feat/nesting-diffs-in-place

Conversation

@YousefED

@YousefED YousefED commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #3167. Draft: the transformer is new. It changes how concurrent moves resolve (see Trade-off).

Problem

blockMatchNodes replaces the full container of a block when the block gets or loses its child group. Thus, the diff of an indent shows three changes:

  1. the parent is deleted;
  2. the parent is inserted;
  3. the moved block is inserted.

Unindent, a new first child and the deletion of a last child also do this. This causes most of the "noisy diff" in version history and suggestions.

The rule has a reason. Two users can give a block its first child at the same time. Without the rule, this puts two blockGroups in one container. The schema (blockContent blockGroup?) cannot show this. Thus, @y/prosemirror drops a group.

Change

  • blockMatchNodes.ts: remove the nesting rule. If only the children of a block change, the container of the block stays.
  • mergeBlockGroups.ts (new): a binding transformer on each blockContainer, as Kevin suggested:
    • The view shows one group. This group contains the children of all non-empty groups of the container. If there are no children, the view hides the group.
    • The transformer sends view edits back to the group that each child came from.
    • When the view deletes the group, the Y groups are emptied but kept. Thus, a child that a different user adds at the same time is not deleted with them. When the user nests a block again, the hidden group fills again.
  • YSync.ts: registers the transformer.
  • utils.ts: yDocToBlocks/yfragmentToBlocks (for example, ServerBlockNoteEditor) and yNodeToTransaction now use the same render pipeline as the binding. Before this change, a block with two child groups caused yDocToBlocks to return an empty document. The yNodeToTransaction change is only for consistency. Its PM-to-PM diffs cannot make two groups.

Covered

Case Before After
indent parent and child deleted and inserted again child moved (deleted at the old position, inserted under the parent)
unindent same child moved
add a nested block / delete the last nested block parent replaced only the child changes
two users give a block its first child at the same time both children kept, parent shown two times in the diff both children under one parent, each with its author
one user deletes the last child, and a different user adds a child at the same time — the added child stays

Baselines updated (reviewed, Chromium/Firefox/WebKit):

  • addRemoveBlocks (delete nested, nest bullet);
  • nesting (indent, unindent);
  • nesting.concurrent (both cases);
  • the moveBlocks HTML snapshot. The group itself no longer has an attribution mark. Its children have the mark. Thus, it renders the same.

Versioning snapshot (tests/src/end-to-end/y-prosemirror/__snapshots__/versioning.test.tsx.snap):

  • Nesting changes ("Indent a block", "Unindent a block", "Nest a bullet under another", "Both nest a new block under N0") show only the moved blocks. The parent no longer shows as deleted and inserted again.
  • "Delete a nested block" shows only the deleted child.
  • blockGroup nodes no longer have their own attribution mark.
  • "Cascading indents" and "Nest blocks into a block that is moved" show the lost blocks as deleted without an author.

Not covered / trade-off

A move is still a delete and a copy. Yjs has no move. This limits the cases that follow:

  • A user writes content into a block that a different user moves at the same time. This content is lost. The gallery shows this:

    • Nest blocks into a block that is moved: A nests B1–B3 under Q. B nests Q under R. B1–B3 are lost. The diff shows them as deleted without an author. With the old rule, Q was shown two times.
    • Cascading indents (note updated): A indents N1. B indents N2 under N1. N2 is lost. The diff shows N2 as deleted without an author: B only moved it, and A never saw it there. With the old rule, N1 was shown two times.

    Thus, the old rule changed these conflicts into duplicates. This PR changes them into loss. In the two cases, no deletion is attributed to a user who did not make it. A correct fix needs move support, or a pair of delete and insert by block id.

  • Known issue: a version deletes the only top-level block of the document (for example, "Delete a parent block" and "Delete parent with mixed children"). The diff pairs the deleted parent with the empty placeholder block of the editor. Thus, the deleted parent loses its block-level delete mark. Its text and its children still show as deleted by A. The versioning snapshot records this.

  • A user changes the type of a parent while a different user edits its child (gallery case "Change a parent's type vs edit its child"): the edit of B is lost. This occurs with and without this PR, because a type change still replaces the block.

  • Enter at the start of a heading (single-user gallery case): the diff shows the heading text as deleted and inserted again in a new block. The split keeps the block id on the empty first half. Thus, this is not a nesting problem. An id check in blockMatchNodes does not fix it.

  • Mixed versions: a client without the transformer can edit the same document. That client sees the extra groups as content that the schema does not allow. It drops them, and it deletes empty groups. feat!: rebuild version history and customize snapshot actions #3090 is not released. Thus, this is important only if the two are released separately.

  • Interaction with fix(versioning): diff structural changes made by the old Yjs binding #3167: splitChangedBlocks no longer splits a nesting change from the old binding, because the nesting rule is removed. Instead, the transformer shows the change in place. The legacy tests of fix(versioning): diff structural changes made by the old Yjs binding #3167 check this. They pass without changes.

  • This PR does not change columns and tables. The transformer merges only blockGroups inside blockContainer.

Tests

The tests of this PR are now in #3090, in nestingChanges.test.ts. The old name of this file was mergeBlockGroups.test.ts. This PR no longer adds a test file.

On #3090, 9 tests have the comment "To be fixed by #3168" and are marked it.fails. This PR changes them to normal tests:

  • nestingChanges.test.ts (7 tests):
    • Concurrent first children merge into one group. Y keeps two groups.
    • yDocToBlocks reads both children of that state.
    • The transformer sends edits to the group of each child.
    • A user adds a child while a different user deletes the last child. The added child stays.
    • When a user nests a block again, the hidden group is used again.
    • The version diff of an indent and of an unindent shows only the moved block.
  • versionDiffAttribution.test.ts (2 cases): a block that is lost to cascading indents shows no author, for the two save orders.

A block's nested children are no longer a reason to replace the block.
A binding transformer shows all of a block's child groups as one group
and keeps emptied groups (hidden) instead of deleting them, so indents,
unindents and concurrent first children diff as moved/added blocks only.
@vercel

vercel Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
blocknote Ready Ready Preview Oct 8, 2026 10:54am UTC
blocknote-website Ready Ready Preview Oct 8, 2026 10:54am UTC

Request Review

@YousefED
YousefED added this pull request to stack #3169 October 8, 2026 04:22
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

yDocToBlocks converted Y without the binding's transformers, so a block
with two child groups (concurrent first children) made the whole
document read as empty.
@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@3168

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@3168

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@3168

@blocknote/diagram-block

npm i https://pkg.pr.new/@blocknote/diagram-block@3168

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@3168

@blocknote/math-block

npm i https://pkg.pr.new/@blocknote/math-block@3168

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@3168

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@3168

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@3168

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@3168

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@3168

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@3168

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@3168

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@3168

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@3168

@blocknote/xl-typst-exporter

npm i https://pkg.pr.new/@blocknote/xl-typst-exporter@3168

commit: 140ce15

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://TypeCellOS.github.io/BlockNote/pr-preview/pr-3168/

Built to branch gh-pages at 2026-10-08 11:10 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

…ffs-in-place

# Conflicts:
#	examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts
#	packages/core/src/y/extensions/versionDiffAttribution.test.ts
…ffs-in-place

# Conflicts:
#	examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts
#	packages/core/src/y/extensions/versionDiffAttribution.test.ts

This branch was successfully deployed

2 active deployments
Preview – blocknote-website — 140ce155 Deployed Oct 8, 2026 by vercel[bot]
Preview – blocknote — 140ce155 Deployed Oct 8, 2026 by vercel[bot]
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