Repository navigation
fix(ui): restore cross-block annotations over list markers and alert titles from drafts - #1542
Merged
Merged
Conversation
…idge block boundaries in the text search A list marker is `select-none`, so no browser puts its bullet or numeral in a selection string: it is absent from the quote an annotation stores. It was not absent from what the highlighter painted, though, so a selection spanning two list items restored onto "a<bullet>b" while its quote read "a\n\nb", the content verification rejected a perfectly good restore, and the annotation came back from a reload with no highlight at all. Marked the marker `annotation-exclude` so the painted text and the stored quote agree by construction, and made the restore comparison read painted text through the same exclusion rather than raw `textContent`, so chrome that ends up inside a wrapper can never reintroduce the asymmetry. The manual text-search wrapper now skips excluded runs too, matching the search that produced its range. The text-search rescue could not bridge a block boundary either: the document's text nodes are joined with nothing between them while the browser's selection string carries a blank line, so a cross-block quote had no match in the whitespace-collapsing fallback. Block starts are now normalized as whitespace on the haystack side, which is the same place the needle already has it.
…range is snapped web-highlighter's stored `textOffset` counts every text node under the parent it records, including `.annotation-exclude` chrome, and its resolver puts a boundary that lands exactly at one text node's end back onto THAT node. A drag from a GitHub alert's icon through its title into its body therefore stored a start of 5 — the length of the hidden "Tip: " — and restored INTO the hidden span, which painting never enters: every run before the last one was dropped, the content verification rejected what was left, and the annotation came back from its own draft with no highlight. Tapped web-highlighter's `Serialize.Restore` hook to move either boundary onto the nearest annotatable text node, refusing the snap if it would invert the range. Normalizing the metas at creation time instead is not available: they are only meaningful in the resolver's own coordinates, which count the excluded text — and it would do nothing for the drafts already on disk.
…ss-block highlights Diagnosis for the Edit Mode symptom: `applyEditedDocument` re-anchors by looking for a block whose content contains the annotation's quote, which no single block does for a quote spanning two — so every cross-block annotation loses its stored positions on every commit, whether or not the edit came near it. That left the text search as the only path, and it could not bridge a block boundary, so one word changed in an unannotated list item unpainted every cross-block comment at once. The cause is the search, fixed in the first commit of this branch; this is the App-level guard over the whole loop: restore a cross-block comment from a draft, edit a word elsewhere through the real edit session, and it is still painted over both paragraphs afterwards.
A markdown restore that fails closed left no user-visible signal: the verification drops a highlight whose stored positions resolved onto the wrong text, the text-search rescue comes up empty, and the comment sits in the panel pointing at nothing while only the console says so. The panel already renders an "Unanchored" chip for exactly this, but it was wired to the HTML surface alone. `useAnnotationHighlighter` now reports each restore pass (`onRestoreReport`: what it attempted, and which of those ended with no highlight), the Viewer forwards it, and the editor keeps a markdown unanchored set beside the HTML one — an id the pass re-anchored clears its own chip, and a document or message change clears the set. Both sets feed the panel prop that already existed.
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two restore shapes that still failed closed after #1535/#1541: a selection spanning two list items, and a drag from a GitHub alert's icon through its title into its body restored from a draft. Both came back from a reload with the comment in the panel and no highlight anywhere in the document.
The two causes
List markers were painted but never selected.
ListMarkerisselect-none, so no browser puts the bullet or numeral in a selection string — it is absent from the quote an annotation stores. It was not absent from what the highlighter painted, so a restore spanning two items resolved ontoa•bwhile its quote reada\n\nb;compactTextonly removes whitespace, the bullet survived it, the content verification rejected a perfectly correct restore, and the text-search rescue could not bridge the blocks either.Stored positions can resolve into excluded chrome. web-highlighter's
textOffsetcounts every text node under the parent it records — the alert's visually hidden"Tip: "included — and its resolver puts a boundary landing exactly at one text node's end back onto that node. The start therefore resumed inside the hidden span, which painting never enters, so every run before the last one was dropped. Creation was already fine (#1541 snaps the live range); only the draft path was.And the rescue could not bridge a block boundary at all. The document's text nodes are joined with nothing between them while the browser's selection string carries a blank line, so no cross-block quote could ever match the whitespace-collapsing fallback.
What changed
ListMarkercarriesannotation-exclude, so the painted text and the stored quote agree by construction. The verification reads painted text through the same exclusion instead of rawtextContent(chrome inside a wrapper can never reintroduce the asymmetry), and the manual text-search wrapper skips excluded runs the way the search that produced its range does. Block starts are normalized as whitespace on the haystack side of the search — the same place the needle already has it.Serialize.Restorehook moves either restored boundary onto the nearest annotatable text node, refusing the snap if it would invert the range. Normalizing the metas at creation time is not available — they are only meaningful in the resolver's own coordinates, which count the excluded text — and it would do nothing for drafts already on disk.useAnnotationHighlighterreports each restore pass (onRestoreReport: what it attempted, which of those ended unanchored), the Viewer forwards it, and the editor keeps a markdown set beside the HTML one; an id the pass re-anchors clears its own chip, and a document or message change clears the set.Edit Mode diagnosis (item 3)
Not a separate bug.
applyEditedDocumentre-anchors by looking for a block whose content contains the quote, which no single block does for a quote spanning two — so every cross-block annotation loses its stored positions on every edit-mode commit, whether or not the edit came near it. Text search was then the only path left, and it could not bridge a boundary, so one word changed in an unannotated list item unpainted every cross-block comment at once ("5 annotations no longer match the text"). Fixed by the search change in commit 1;packages/editor/App.editModeCrossBlockHighlights.test.tsxpins the whole loop through the real App.Browser verification
Re-ran the QA harness against this build (plan review + annotate, ports 19850-19855), same seven shapes, same fixtures:
Zero restore warnings on the console on either surface (previously four, for C and E). Export quotes unchanged and correct on both:
"First list item about caching behaviour\nSecond list item about retry"with no bullet,"Browser quirks\nSafari clamps the selection"with no hidden"Tip: ", and the reviewer's own"Tip: real prose"intact. Edit Mode: 7 highlights before an edit to an unannotated list item, 7 after (previously 5 → 2). Drift is still fail-closed — positions drifted with the quote still present are rescued onto the right text, positions drifted with the quote gone paint nothing and now carry the Unanchored chip.Checks
bun run typecheck,DOM_TESTS=1 bun test packages/ui packages/editor(1732 pass; the 4DocBadgesfailures are pre-existing onorigin/mainin the same whole-suite run and pass in isolation),bun test scripts/dom-test-allowlist.test.ts,bun run --cwd packages/ui smoke:package,bun run --cwd apps/guides-show check:manifest.Three DOM-gated test files added and registered in
.github/workflows/test.yml;Viewer.crossBlockRestore.test.tsx's "two list items" fixture was corrected to the real browser shape (it previously put the bullet intooriginalText, which no browser does, and passed for the wrong reason).