fix(viewer): fold state and annotation visibility (#7, #8) - #9
Open
folded wants to merge 3 commits into
Open
Conversation
Repro (#8): fold a class, fold the file, unfold the file — the class came back with an open chevron over hidden rows, so the hunk showed annotation and comment rows with a gap in the line-number gutter. Collapsing a region only wrote `display: none` onto row elements. A file or hunk toggle re-renders, which rebuilds every chevron in the open state but serves the `.diff` from render.ts's `renderedDiffs` cache — the same elements, still carrying the old inline display. The DOM was the only record of the fold, and half of it survived the re-render. Collapse state now lives on the region record as `_folded` (alongside `summary` / `_inflight`, which already persist there), and `_applyFoldState` derives row visibility, chevron direction and summary-row visibility for the whole file from those flags. The pass runs on every attach, so a re-render restores the fold instead of inheriting a half-state; a fold the reviewer set survives a file or hunk toggle the way a file/hunk override already does. Whole-file rather than per-region because regions nest: a row is hidden when any enclosing region is folded. The old per-region toggle revealed the body of a still-folded inner region when the outer one opened (second test).
Repro (#7): fold a class and the line notes inside it stayed on screen, full width, arrows pointing at the far-left edge of the annotation cell. A fold hid the code rows but not the annotation rows hanging off them, so the notes outlived their anchors. The arrow x comes from `charCenterAt` → `charRangeRect`, which measures the anchor's text; a collapsed anchor has no layout box, so every rect reads zero, the resolved x is 0, and `marginLeft` clamps to the left edge. Nothing had to reflow the note for this to bite: `render()` schedules a `reflowAll()` on every render, and a fold toggle re-renders. Two changes, because the annotation was both wrongly visible and wrongly measured: - `setAnchorVisible(anchor, visible)` hides or shows the annotations (and shadow placeholders) belonging to one anchor, and `attach()` starts an annotation hidden under an already-collapsed anchor — so the comment replay that follows every render can't reinstate a comment over folded-away code. `_applyFoldState` calls it for each row whose visibility it changes, after which it re-asserts each region's own summary row (a nested region's summary hangs off a header row inside the enclosing body). - `sizeAnnotArrow` bails when the anchor has no layout box, keeping the last known geometry instead of recording a zero. Any other path that measures an unmounted or collapsed anchor is now inert rather than destructive.
The fold-region entry covered addressing and summaries but not who owns the collapsed bit, which is the part that reads as a bug when a fold survives a re-render and not a reload.
This was referenced Aug 19, 2026
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.
Fixes #7 and #8. Both turned out to be deeper than the issues described, and #7's root cause is the opposite of the hypothesis I put in the issue.
#8 — fold state desyncs
Collapse state existed only as
display: noneon row elements. A file or hunk toggle callsrender(), which rebuilds chevrons in the open state but serves the.difffrom_state.renderedDiffs— the same elements, still carrying the stale inline display. The DOM was the only record of the fold and only half of it survived a re-render.State now lives on the region record (
FoldRegion._folded, alongsidesummary/_inflight), and_applyFoldStatederives row visibility, chevron direction and summary-row visibility for the whole file from those flags on every attach.Whole-file rather than per-region because regions nest — which also fixed a bug neither issue mentions: opening an outer region used to reveal a still-folded inner region's body.
#7 — annotations lose column attachment
The hypothesis in the issue was right about the mechanism and wrong about the cause. A collapsed anchor has no layout box, every rect reads zero,
charCenterAtreturns 0 and the box clamps to the left edge — that part holds. But it is not a missing reflow:render()already schedulesreflowAll()after every render, and folds hid the code rows while leaving the annotation rows visible, so each later render re-measured a note whose anchor was hidden and destroyed its x. Too much reflow while hidden, not too little.Annotations.setAnchorVisible(anchor, visible)— annotations and their shadow placeholders travel with their anchor;attach()starts hidden under an already-collapsed anchor, so the comment replay after each render cannot reinstate a comment over folded-away code.sizeAnnotArrowbails when the anchor has no layout box, keeping last-known geometry rather than writing a zero.Verification
184 JS tests (177 + 7 new), 663 Python tests,
tsc --noEmitclean. 4 of the newannotations.test.tscases were confirmed failing against the pre-fix sources.Not verified: that arrows visually land on the right column in a real browser after unfolding. jsdom has no layout, so the tests can only confirm that hidden anchors no longer overwrite geometry and that a reflow is scheduled when the anchor returns. This wants one manual pass in the viewer.
Pre-existing hazards found, not addressed
_findExistingFoldRecordmatches regions by context + line ranges, so a gap expansion that changes a region's span creates a fresh record and drops_folded. That is the limit of the persistence, not a regression._FILE_FOLD_STATEis not torn down when a file folds, leaving handles and ResizeObservers on detached DOM until the next attach.