Skip to content

fix(viewer): fold state and annotation visibility (#7, #8) - #9

Open
folded wants to merge 3 commits into
mainfrom
fix/viewer-fold-state
Open

fix(viewer): fold state and annotation visibility (#7, #8)#9
folded wants to merge 3 commits into
mainfrom
fix/viewer-fold-state

Conversation

@folded

@folded folded commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

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: none on row elements. A file or hunk toggle calls render(), which rebuilds chevrons in the open state but serves the .diff from _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, alongside summary/_inflight), and _applyFoldState derives 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, charCenterAt returns 0 and the box clamps to the left edge — that part holds. But it is not a missing reflow: render() already schedules reflowAll() 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.
  • sizeAnnotArrow bails 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 --noEmit clean. 4 of the new annotations.test.ts cases 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

  • A region restored as folded whose summary never arrived shows the seeded "summarising…" text with no request behind it until the next click. Firing a request on restore would re-POST a failed one on every render.
  • _findExistingFoldRecord matches 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_STATE is not torn down when a file folds, leaving handles and ResizeObservers on detached DOM until the next attach.

folded added 3 commits August 19, 2026 17:53
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.
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.

viewer: annotations lose their column attachment after folding

1 participant