feat(panel): the diff bundle draws a whole file, and notes under one patch's lines
Summary
This is PR 1 of the approved plan Pierre for File tabs and code snippets (plan page). It adds the two capabilities the rest of the stack needs to the lazy diff bundle (packages/panel/src/diff-view/). Nothing in the app calls them yet, so nothing visible changes.
mountFile(host, props)draws a whole file with no diff, as the File tab, a page's source and the text-attachment viewer will (PR 2 and PR 5).- How it draws: Pierre's
CodeViewholds onetype: 'file'item. Only the lines near the view are drawn, whatever the file's length. Colours come from the same kept worker pool as Files changed (#3752), and the file shows as plain text until they arrive. - Cache key: the file's name, revision and text pick its key in the pool's cache, so reopening the same file at the same revision gets its colours straight away. A count names the key, never the identifiers, per the TypeScript rules in
AGENTS.md. - Lit lines:
lituses Pierre's own selected lines, the same call Files changed uses to light a thread's lines. The bundle sets them withnotify: false, so Pierre doesn't report them as the reader's pick. - Notes: each annotation goes under its line, in the element the view keeps for it (
renderAnnotation(key)), as the list does. pickedLines(): returns the lines picked by pressing their numbers (GitHub-style, with ⇧ to extend). Otherwise it returns the lines a text selection in the shadow root covers. ⌥⌘K will use this.revealLines(range): centres a range at once.- Find and screen readers: the shadow host carries
FIND_SHADOW_ATTRIBUTE, so the tab's find reads it and skips the gutter. The gutter is markedaria-hidden, so a screen reader doesn't read every number and then every line. - Trailing newline: a text that ends in a newline draws no extra empty line.
onDrawn: called whenever Pierre draws rows, so a find source can repaint its matches (PR 2).
- How it draws: Pierre's
mounttakesannotationsandrenderAnnotationagain. #3304 removed them. The Code tab and inbox cards (PR 4) use them to put Luke's note under a snippet's last line.
No service change.
Measured
This is the same throwaway harness as the plan, now running the built diff-view.js's mountFile, with the CSSOM inline-style path and the build's worker. Setup: a 5,000-line TSX file in headless Chromium at 1280×800, a 720px pane, median of 3. Today's numbers come from today's CodeLinesView given every window up front with host colours. Both columns were measured in the same session.
| today (windows, every line read stays drawn) | mountFile | |
|---|---|---|
| Fling (400 px/frame, 4 s): long tasks · longest frame · frames | 705 ms · 117 ms · 207 | 0 ms · 16.8 ms · 242 |
| Find open (all 5,000 drawn): long tasks on load | 1,121 ms | n/a (PR 2 searches the text) |
| Long tasks on load | 183 ms | 0 ms |
| First lines painted, bundle import included | 64 ms | 149–169 ms (plain) |
| Lines in the DOM | 500 → 5,000 | 54–67 |
| With 20 notes, wrapped, fling | — | 0 ms · 16.8 ms · 242 |
First colour took about 3.3 s on this session's slower sandbox. That matches the plan's prototype measured in the same session (3.4–3.5 s), so the real bundle adds nothing on top of Pierre. The developer accepted late colours on big files.
renderer/diff-view.js: 243,477 → 245,160 gzipped bytes (+1.7 KB), inside the budget's 5% slack. panel.js is unchanged.
Tests
New file diff-view/whole-file.test.ts (8 tests). Pierre draws in jsdom on the main thread:
- A whole file is numbered from line 1, with no empty line after its final newline.
- Its numbers are left out of the find and of what a screen reader reads.
- Lit lines are lit, other lit lines replace them, and none remain once cleared.
- Each note is drawn in the view's element under its line, and moves as the notes change.
- A file whose text changes draws its new lines.
- No lines are picked until the reader picks some.
- Lines picked by pressing their numbers (with ⇧) are the reader's pick, until the view lights others.
- A patch drawn alone draws each note in the view's element under its line.
The four stand-in bundles in the tab tests gain mountFile.
Evidence
tsc --noEmit(panel): clean../scripts/oxlint.sh: 0 warnings, 0 errors.biome check: clean.knip: no new findings../scripts/repository-checks.sh: passed.- vitest:
src/diff-viewplus the changes, chapters, pull request and themed-lines tab suites, 14 files, 235 tests passed. apps/desktopbuild: passed.
Invariants
- Every behavior change has the test that fails without it; no change-detector test.
- No new
astype assertion, raw async primitive, or Effect run outside the edges. -
PRIVACY.mdunchanged: the file view's root stays under the File tab'sph-no-capture(PR 2).
Made with Luke