Skip to content

Keep the open preview editor mounted across offset-shifting remote CRDT changes - #618

Merged
cscheid merged 1 commit into
mainfrom
bugfix/bd-84ljmbaf-rich-text-editor-remounts
Aug 26, 2026
Merged

Keep the open preview editor mounted across offset-shifting remote CRDT changes#618
cscheid merged 1 commit into
mainfrom
bugfix/bd-84ljmbaf-rich-text-editor-remounts

Conversation

@cscheid

@cscheid cscheid commented Aug 26, 2026

Copy link
Copy Markdown
Member

Fixes #420 (braid bd-84ljmbaf).

Problem

With the rich-text editor open in q2 preview --allow-edit (or hub-client), any remote CRDT change that shifts the active block's byte offsets — i.e. any length-changing edit anywhere earlier in the document — disturbed the editor:

  • a focusout/focusin blip (~30 ms) that drops keystrokes typed in the window,
  • caret reset to end-of-block, undo history lost,
  • on the rich surface, silent loss of the uncommitted draft (it reseeds from the AST; the textarea reseeds from the preserved editDraftRef and only blips).

Root cause

The first render after an external re-render still carried the stale editTarget.anchorR0. The dispatcher's anchor match (editTarget.anchorR0 === resolved.sourceEntry.r[0]) then failed for every block, unmounting the editor for one frame; the P2.3b self-heal useLayoutEffect re-anchored one render too late and mounted a fresh editor. The re-anchor logic (findReanchorCandidate, content-first and symmetric) was fine — only its timing was wrong.

Fix

PreviewRoot now derives the re-anchored target during render (effectiveEditTarget, a useMemo running findReanchorCandidate over the freshly-parsed pool/content) and feeds it to the context and editTargetRef. The first post-change render already matches the shifted offsets, so React reconciles the mounted editor in place — same DOM node, focus never leaves, caret/selection/doc untouched. The self-heal effect still persists the re-anchor into state and still owns the DROP path (a remote edit to the active block itself closes the editor, unchanged).

Testing

  • New editor-survives-remote-shift.integration.test.tsx (real PreviewRoot, real pointer-event activation) pins element identity (toBe) and document.activeElement retention across forward and backward shifts, for the rich surface and the textarea (the textarea test also pins the dirty draft). Written first per TDD: 3 tests failed on the unfixed tree exactly on the remount assertions; all 4 pass with the fix.
  • preview-renderer suites: 578 unit + 632 integration passing. Full cargo xtask verify (including hub-client build:all + test:ci): green.
  • End-to-end through the real binary: q2 preview --allow-edit in Chrome — typed a draft into the rich editor, applied two offset-shifting edits to the file on disk (title grow, then shrink); the ProseMirror DOM instance survived both, zero focus events, caret and draft intact, and a subsequent Cmd-Enter commit landed on the correct paragraph with the remote edits preserved.

Out of scope (follow-up strands filed)

  • bd-vrdglm6m — a whole-block insert/delete above the active block still remounts (index-based key={i} shifts).
  • bd-yu5uj8am — merge remote content into the open editor instead of the self-heal DROP when the active block is edited remotely.

Plan/diagnosis: claude-notes/plans/2026-08-26-gh420-editor-focus-crdt.md.

🤖 Generated with Claude Code

…te changes

Fixes the GH #420 focus loss: an incoming CRDT change that shifted the
active block's byte offsets rendered one frame against the stale
editTarget.anchorR0, so the dispatcher's anchor match failed, the
editor (rich tiptap surface or textarea) unmounted, and the P2.3b
self-heal layout effect then mounted a fresh one — a focusout/focusin
blip that dropped keystrokes, reset the caret to end-of-block, and (on
the rich surface, which reseeds from the AST rather than editDraftRef)
silently discarded the uncommitted draft.

PreviewRoot now derives the re-anchored target during render
(effectiveEditTarget: findReanchorCandidate over the freshly-parsed
pool/content), feeds it to the context and editTargetRef, so the first
post-change render already matches the shifted offsets and React
reconciles the mounted editor in place. The self-heal effect still
persists the re-anchor into state and still owns the DROP path
(unchanged: a remote edit to the active block itself closes the
editor).

TDD: editor-survives-remote-shift.integration.test.tsx pins element
identity + focus retention across forward and backward shifts for both
surfaces (3 tests failed on the unfixed tree, all pass now). Verified
end-to-end through q2 preview --allow-edit in Chrome: same ProseMirror
instance, no focus events, caret and typed draft intact across two
offset-shifting disk edits, and a subsequent Cmd-Enter commit landed on
the correct paragraph.

Follow-ups filed: bd-vrdglm6m (block insert/delete above still remounts
via index keys), bd-yu5uj8am (merge instead of DROP for remote edits to
the active block). Plan:
claude-notes/plans/2026-08-26-gh420-editor-focus-crdt.md

Closes #420.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cscheid
cscheid merged commit 5eb4ad1 into main Aug 26, 2026
10 checks passed
@cscheid
cscheid deleted the bugfix/bd-84ljmbaf-rich-text-editor-remounts branch August 26, 2026 22:19
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.

preview rich text editor loses focus on incoming CRDT changes

2 participants