Fix comment overlay cloning block ancestors on repaint - #20
Merged
Conversation
The overlay wrapped each highlight segment with a single span via Range.extractContents()/insertNode(). When a segment's range straddled an element boundary (a highlight starting inside a callout and ending in the next block), extractContents cloned the partially-selected ancestor into the wrapper. clearHighlights() only unwraps our own [data-jh-seg] spans, so the cloned empty blocks survived and accumulated one per repaint — and paint runs on resize and on every anchors/reactions message, so the stray blocks (empty callouts rendering as growing grey bars) piled up over time. Wrap one text node at a time instead: a per-node range can only split its own text node, never partially-select an element, so no ancestor is cloned and the paint is fully reversible by clearHighlights. A segment spanning several nodes becomes several adjacent spans sharing the same cover; the chip/position/focus code already treats segEls as a set. This also subsumes the old start-bias workaround, since we never wrap a block element and a highlight at a block's leading edge can no longer vanish. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
tnsardesai
marked this pull request as ready for review
July 29, 2026 01:23
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.
Problem
In the sandboxed doc overlay, a highlight whose range straddles an element boundary (e.g. one that starts inside a
.calloutand ends in the next block) makes empty block clones pile up in the DOM — one per repaint. Becausepaint()runs onresizeand on everyjh:anchors/jh:reactionsmessage, the stray blocks accumulate over time and render as empty grey bars that keep growing above the content.Root cause
paint()wrapped each highlight segment with a single span viaRange.extractContents()+insertNode(). When the segment's range spans an element boundary,extractContents()clones the partially-selected ancestor into the fragment (standard DOM behavior — cloning the parent tags needed to keep the fragment valid).clearHighlights()only unwraps our own[data-jh-seg]spans, so those cloned empty blocks survive every clear and re-clone on the next paint.Fix
Wrap one text node at a time (
sliceNodes) instead of one span across the whole segment range. A per-node range can only ever split its own text node — it never partially-selects an element, so no ancestor is cloned and the paint is fully reversible byclearHighlights(). A segment spanning several nodes becomes several adjacent spans sharing the samedata-cover(identical styling); the chip, position, and focus code already treatsegElsas a set, so this is transparent to them. This also subsumes the old start-bias workaround (locateStart), since we never wrap a block element and a highlight at a block's leading edge can no longer collapse into an unpainted inline span.Net:
-43 / +36in one file, and three now-dead helpers (locate,locateStart,mkRange) are removed.Verification
Existing suite:
162 passed.tsc --noEmitclean.Behavioral before/after in a real headless Chrome — same doc (a
.calloutheader whose highlight straddles into the next block), 8 repaints:The fix stops the accumulation while still painting the highlight across the block boundary.
🤖 Generated with Claude Code
Note
Medium Risk
Changes core DOM painting for all comment/reaction highlights in the overlay iframe; behavior is localized to one file but affects every repaint path.
Overview
Fixes accumulating empty block elements (e.g. grey bars above content) when comment/reaction highlights are repainted in the sandboxed doc overlay.
paint()no longer wraps each segment with onedata-jh-segspan over a multi-nodeRange. It usessliceNodesto apply **extractContents/insertNodeonly within a single text node at a time. That avoidsRange.extractContents()cloning straddled ancestors (callouts, paragraphs) into the wrapper—clones thatclearHighlights()could not remove and that stacked on every resize orjh:anchors/jh:reactionsrepaint.A logical segment that crosses text nodes becomes several adjacent spans with the same
data-coverand depth styling; chip, position, and focus logic already treatsegElsas a set.locate,locateStart, andmkRangeare removed; the old start-bias workaround is unnecessary because block elements are never wrapped.Reviewed by Cursor Bugbot for commit 224c05e. Bugbot is set up for automated code reviews on this repo. Configure here.