Fix visible artefacts when resizing the annot window (Windows)#3
Open
nikhilkrishna wants to merge 34 commits into
Open
Fix visible artefacts when resizing the annot window (Windows)#3nikhilkrishna wants to merge 34 commits into
nikhilkrishna wants to merge 34 commits into
Conversation
validateRange rejects any range whose consecutive line numbers jump by more than 1 — a check that exists to detect portal boundaries in markdown mode. But diff lines number removed rows in old-file coordinates and added/context rows in new-file coordinates (getLineNumber returns new_line ?? old_line), so a hunk like context new=124, removed old=123, added new=125 reads as a gap of 2. Any annotation covering a removed line next to an added one was silently dropped with only a console warning — broken since the initial commit. Skip the discontinuity check for diff-origin lines. Diffs have no portals to guard against, and cross-hunk or cross-file selections are still rejected: hunk and file header lines carry no line numbers, and all lines in a range must share one path.
Cmd/Ctrl+B toggles a sidebar listing every changed file in a diff with its +/- counts. Clicking a file scrolls to it; the row for the file currently in view stays highlighted. The `:` palette gains a Files namespace that does the same jump by fuzzy search, hidden outside diff sessions. Navigation reads DiffMetadata.files, which was already on the wire and unused. deriveFileEntries() in src/lib/file-tree.ts is the single seam binding the sidebar to the diff wire model. Also fixes current-file tracking, which was wrong before this feature existed: useContentTracking derived the current file from hunk boundaries alone, but a file's header lines sit above its first hunk, so any header line resolved to the *previous* file — visible in the Header breadcrumb when scrolling a file header to the top of the viewport. A separate file-only ContentTracker now resolves currentFileIndex. hunkTracker is left hunk-only because useSelectionBounds walks its boundaries pairwise to clamp selections to a hunk.
…denolehov#88) * chore: untrack specs * feat(diff): per-file collapse with sticky headers + changeset summary Every file in a diff gets a sticky header bar (chevron, dir-muted path, +/- counts) that collapses the file to just its header. Files with more than 500 changed lines start collapsed. The titlebar shows 'N files changed, +A -D' with a fold-all/unfold-all toggle; search hits and file jumps auto-expand collapsed files. Collapse is pure presentation: the lines array is never mutated, so display-index-keyed annotations keep resolving to the same content. Backend: split the lumped DiffLineKind::Header into FileHeader (the diff --git line), HunkHeader (@@, now constructing DiffSemantics:: HunkHeader with function context), and Meta (index/---/+++/mode lines, never rendered, skipped by search, non-selectable). Fix current-hunk tracking under sticky headers: the position probe now uses elementsFromPoint to prefer the line row covered by a stuck header instead of freezing on the file's first hunk.
Replace the fixed 240px flex-basis with a paneforge PaneGroup/Pane/ PaneResizer split so the diff-mode file tree can be dragged wider or narrower, clamped to 12-45% of the viewer width. Width resets to the 18%->22% default on relaunch — no persistence, matching the existing sidebar open/close state convention.
"N files changed" had no overflow handling, so it wrapped/overflowed once the sidebar was dragged narrow. Ellipsis-truncate it the same way .file-row-dir already does for long paths.
Three bugs in the sidebar's jump-to-file path, all in scrollToDisplayIndex/ jumpToFile: - jumpToFile expanded a collapsed file synchronously and scrolled in the same tick, so scrollIntoView ran against the still-collapsed DOM layout. Now awaits tick() after expanding. - Native scrollIntoView miscalculates against sticky file-header rows (position: sticky; top: 0) when jumping backward to an earlier file, landing short and making the target file's body appear empty. The 'start' case now computes the scroll delta directly via getBoundingClientRect instead. - The sidebar's current-file highlight only updated via a scroll-driven hit-test, which could race against the DOM churn from expanding a collapsed file. jumpToFile now updates current-file tracking directly.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Add compute_hunks(old, new, context): hunks computed from two full texts instead of parsed from patch output. Built on `similar` (already a dep) behind the signature as the contract, so the crate stays swappable. - Line diff over split_inclusive tokens: terminator-sensitive, so a missing trailing newline diffs like git - Context-line merging of adjacent hunks, mirroring git - Word-level byte ranges, gated Zed-style: only hunks with equal deleted/added counts of at most 5 lines - Corpus insta snapshots + proptest round-trip (old + hunks == new); hunk boundaries spot-checked against git diff --no-index diff.rs stays the legacy patch parser for raw diff_content until C1.
review_diff's `git_diff_args` parameter is replaced by a structured
`target` (working_tree | staged | range{from, to, merge_base}) plus
optional `pathspecs`; MCP clients must update their tool calls.
FileSource trait (side -> full text | None) with GixSource (odb reads
gated by header kind/size, 1 MiB cap, NUL/UTF-8 binary gate, oid-keyed
cache) and RawPatchSource for raw patches. vcs::enumerate lists changed
files for the structured target. WorkingTree fuses the HEAD-vs-index and
index-vs-worktree status layers per path (pure, table-tested merge fn)
and includes untracked files as Added. Standalone substrate: the render
pipeline still consumes patch text (args composed from the target)
until B4 swaps it.
Annotations move from position-keyed (HashMap<LineRange, Annotation>) to
identity-keyed (IndexMap<String, Annotation>): id is the identity, anchor
{path, start: Endpoint{side, line}, end: Endpoint{side, line}} is a mutable
property. Unlocks threads, re-anchoring after re-diff, and split-view (S4)
without a model change — none of which was representable when position was
the key.
- New anchor.rs: Endpoint/Anchor/Annotation, reusing source::Side.
- AnnotationTarget::upsert_annotation displaces any other id already
holding the same anchor — an anchor can only be claimed by one id.
- IPC (upsert_annotation/delete_annotation) takes id + anchor.
- Output layer unchanged behaviorally: Annotation::start_line()/end_line()
accessors keep formatters/sorting byte-identical — zero snapshot diffs.
- Frontend: useAnnotations.svelte.ts mints an id once per annotation and
reuses it across edits; side is derived per range endpoint (a range's
two ends can sit on different sides of a diff hunk).
- Dead AppState/SessionState annotation duplicate removed (zero callers).
Also includes an incidental whole-crate cargo fmt pass (~19 files, cosmetic
only, kept per request) picked up mid-session.
…ted in-process
Structured-target review_diff no longer shells `git diff` and parses
patch text: vcs::enumerate lists changed files, GixSource serves both
sides' full texts, compute_hunks derives the hunks, and pipeline.rs
synthesizes the same patch-shaped Line stream — wire contract
unchanged, existing output snapshots untouched. to_git_args is gone;
raw diff_content keeps the legacy patch parser.
- Untracked files now render in working-tree reviews (all-added hunks)
- Hunk-header function context via git's default driver rule (nearest
[alpha_$]-starting line above the hunk, old side, 80-byte cap) —
gix has no userdiff support
- ContentModel owns file_source (RawPatchSource everywhere except the
git pipeline's GixSource), keeping full texts alive per session for
unfold and re-diff
- Binary/oversize/non-UTF-8 files degrade to header + meta notice,
matching the legacy rendering
- Shared helpers extracted: diff::language_for,
Highlighter::{highlight_diff_row, highlight_function_context}
- Parity tests vs the legacy parser exposed two of its latent bugs the
pipeline fixes: rename-only sections misindex every following file;
the `\ No newline` marker skews subsequent new-side line numbers.
Manual corpus harness: side_by_side_on_this_repo (--ignored)
…(old:2 → new:5) Anchor endpoints now resolve to display rows by side (ContentModel::find_row) and annotations render the contiguous row slice — replacing the numeric loop through side-blind find_line, which could pick old:N rows for new:N anchors. Mixed-side ranges (deleted line + added replacement) get a distinct header, `file.rs (old:2 → new:5):`, a shape no single-side or context annotation can produce; all existing header shapes and snapshots are byte-identical. Snapshot corpus: old-only / new-only / mixed × single- and multi-line. Deleted-line test fixtures corrected to Side::Old (what the frontend sends).
Sides only exist where a diff does — the single-struct Anchor with
degenerate side: New for file/content/markdown modes made garbage
anchors (old-side in markdown) representable. Split into Source
{path, start, end} and Diff {path, start: Endpoint, end: Endpoint},
serde-tagged "type": source|diff to match the LineOrigin convention.
Frontend adapter picks the variant from the start line origin; the
diff formatter matches out the Diff variant and degrades a side-less
anchor to the existing header-only fallback. Output byte-identical —
zero snapshot churn. Amends A1.
… anchors
Lands A2 of the diff redesign. Annotation identity was a display-index
range string ("363-371") — store key, undo history, editor state, and
event routing all hung off it, so any reshaping of the lines array
(per-file documents, unfold) would dangle every key.
Now the id is the identity and the anchor (source coordinates, side-
aware for diffs) is the position, mirroring the backend model. Display
rows are resolved at render time through a lines-derived endpoint map
and never persisted. Anchors are computed from the selection at
creation (selectionToAnchor absorbs validateRange); rangeToKey/
keyToRange die.
New-annotation identity is a draft {id, anchor} minted when the slot
appears and shadowing any open editor, so the draft-to-saved transition
(and emptying an entry mid-edit) never remounts the live editor.
Undo/redo now diffs entity snapshots by id and syncs deletions and
re-upserts to the backend, closing the known restore gap.
The excalidraw window wire field range_key is renamed annotation_id
end to end — it carries the annotation id now.
Also hardens the create-tag selection popover: re-read the live
selection when the debounce fires (stale positions crashed textBetween
after select-all + delete).
Lands B5 of the diff redesign. `diff.rs::parse_diff` hand-walked raw text and prefix-sniffed line kinds; `unidiff` was only used for file-list validation. C1 needs both diff producers (this one and B4's `pipeline.rs`) shaped as per-file internal models before it can do pure exposure + deletion — this brings the raw-patch path to parity. Swapped `unidiff` for `diffy`'s `patch_set` (`ParseOptions::gitdiff()`), which models git's extended headers (rename/copy/mode/binary) as typed data instead of string-sniffed plumbing. `parse_diff` now builds a per-file model (`RawDiffFile`/`RawHunk`/`RawDiffRow`, prefix-stripped content) directly from diffy's `FileOperation`/`Hunk`/`Line`; a single pure `flatten` reproduces the legacy `Vec<Line>` + `DiffMetadata` wire shape. `DiffLineKind`/`DiffLineInfo`/`DiffMetadata.lines` are deleted outright rather than deferred to C1 — nothing outside the old construction path ever read them. The swap also fixes two latent parser bugs as a verified side effect: pure-rename sections (no hunks) used to silently drop from the file list and misindex everything after them; `\ No newline at end of file` used to get miscounted as a context line, shifting subsequent line numbers. Neither is covered by an existing snapshot, and both are now regression-tested directly. All 336 tests and every insta snapshot are unchanged; sample.diff (pnpm demo:diff) parses end-to-end identically.
One derived walk becomes the single source of display truth for diff mode. While the wire stays flat, synthesizeDocs promotes lines + metadata.files into per-file documents (hunks owning rows); the walk stamps wire-space display indexes via a wireIndex ?? position fallback so consumers can migrate one at a time without splitting index spaces. Equivalence tests pin the walk to the old projections (deriveFileEntries, groupByFile, wire indexes) and prove the positional fallback for the future per-file wire input.
deriveFileEntries leaves the page: the DisplayRow derivation feeds a transitional FileEntry adapter for consumers not yet on the walk, while FileTree and the palette files namespace consume DocViews directly (counts, headerDisplayIndex). diffDisplay joins the annot context.
RegularLines' diff path iterates DisplayRow entries grouped per document: file headers render structurally from DocViews, hunk headers regenerate their @@ text from hunk ranges, rows draw from walk Row data — groupByFile leaves the render path. useFileCollapse keys off DocViews and absorbs the auto-collapse threshold; collapse stays a render-time visibility skip, so display indexes are stable under toggle.
The walk records each hunk's display footprint (header index + row span); useSelectionBounds resolves the anchor through byIndex and clamps to that span, dropping its hunkTracker/DiffMetadata dependencies. Row entries carry hunkIdx so any display position resolves to its hunk in O(1).
useContentTracking drops its hunk/file ContentTrackers — a display index resolves to (file, hunk) via the walk's byIndex in O(1), with file headers owning their file's position. Header shows the current DocView and HunkV2 ranges; ContentTracker survives for markdown sections only.
Search matches diff content entry-by-entry — file headers match their rendered path (not the raw 'diff --git' text), hunk headers their regenerated marker, rows their rendered text. Scroll targeting and file jumps resolve documents via byIndex/DocViews; fileContaining leaves the page.
endpointToRow reads the walk's byEndpoint in diff mode, keyed by the shared anchor.ts diffKey; selectionToDiffAnchor builds diff anchors from walk rows (headers and cross-document spans reject). selectionToAnchor, endpointKeys, and isSelectable shed their diff branches — the walk's entry kind is the only diff selectability truth. getSide dies.
…nly diff truth Header's collapse-all gate reads DocViews; the transitional FileEntry adapter survives only as the equivalence test's oracle. deriveFileEntries, groupByFile, and getDiffKind now have no consumers outside their dying modules and tests.
ContentView{Flat|Diff}, DiffDocument, HunkV2, Row land in state.rs with no
producers or consumers yet. FileStatus serializes as a plain string — the
wire doesn't carry rename similarity.
…he flat diff stream dies Both producers now return Vec<DiffDocument>: pipeline::render keeps compute_hunks' ranges (printed-normalized so an empty side reads -0,0), diff.rs builds documents straight from diffy and loses its Raw* layer. Rows carry raw content and raw html — highlight_diff_row drops its prefix parameter. Deleted wholesale: flatten/flatten_file/flatten_hunk, DiffMetadata, DiffFileInfo, HunkInfo, ContentMetadata::Diff, LineOrigin::Diff, LineSemantics::Diff + DiffSemantics, ContentModel::find_row. ContentModel/ContentResponse carry view: ContentView; markdown/portal paths read flat_lines(). Output resolves anchors per-document (FileKey::diff_file(index) ⇔ documents[index]), context is hunk-local, and the +/-/space sign is re-prepended at emit from the row's line-number pattern — all output/ insta snapshots pass byte-identical.
…urce The settled cosmetic lands with the raw-content wire: the sign renders as a dedicated gutter column from the walk's rowKind (user-select: none, so copied text stays raw), colored per side like the line numbers. Under the hood this is the frontend join: get_content unpacks view — flat lines or diff documents; deriveDisplay consumes DiffDocument[] directly and stamps dense positional indexes. Deleted: synthesizeDocs + Pseudo* shims, toFileEntries, file-tree.ts, file-collapse.ts, flat-Line diff origin/semantics and their line-utils branches, the DiffMetadata/DiffFileInfo/HunkInfo TS mirrors.
(WebView2), found while chasing content that visibly trails the resize: 1. Set background_color on every window builder to match the theme's --bg-main (light #fafaf9 / dark #1d1b16, System→light). WebView2 was painting its default white on window-open and at newly-exposed edges during resize; this paints a solid themed background on both the window and webview layers immediately. Regression test added. 2. content-visibility: auto on div.line so off-screen lines are skipped during layout. On a 10k-line file a resize re-wrapped every line every frame; forced-layout median dropped from 129ms to 27ms (4.7x), p95 178ms→31ms, max 303ms→33ms (measured via CDP in-document A/B). Content and the bottom status bar no longer trail the resize as severely.
On a 10k-line file (~112k DOM nodes) the per-frame style+layout walk can't keep up with a window resize, so content visibly trailed the dragged edge. Measured the cost breakdown via CDP: style-recalc, not layout, dominates, and it's proportional to node count — no pure-CSS reflow tuning closes the gap. Fix: while the window is GROWING, add a `.resizing` class that sets content-visibility: hidden on the content tree (~7x cheaper per frame, measured), freezing the last-painted frame; the outer scroller keeps a themed background so the newly-exposed area shows clean empty space, and content snaps back ~120ms after the drag settles. Grow-only on purpose: shrinking has no trailing edge, and freezing a fast shrink outran the browser's cached paint and flashed blank. Also drop the status bar's backdrop-filter: its background is opaque, so the blur had zero visual effect but cost a full-width blur pass every resize frame. And fix div.line contain-intrinsic-size to the real 22px line-height so off-screen height estimates are accurate. Small/medium files were already smooth and stay so.
Replace the manual globalThis addEventListener/removeEventListener + onDestroy plumbing with the <svelte:window onresize> binding Svelte already provides. Behavior-identical (same debounced handler, same reactive .resizing class), -8 lines, and Svelte owns listener teardown.
The background_color fix targets WebView2's white flash, a Windows-only symptom. macOS ignores background_color (Tauri no-op) and Linux/WebKitGTK never flashed, so scope it to #[cfg(windows)] at all four window builders, matching the existing per-OS builder rebinds. The config helper and its test are gated too, so non-Windows builds compile clean with no dead code.
FileRefChip's copy-to-clipboard action was silent (TODO left in place). Wires a bubbling `file-ref-copied` CustomEvent from the nodeview through AnnotationEditor's existing listener pattern (mirrors excalidraw-edit) and threads an onFileRefCopied callback through AnnotationSlot/SessionEditor to +page.svelte's toast, following the onImagePasteBlocked precedent.
denolehov
force-pushed
the
ui_performance
branch
from
July 11, 2026 18:14
24a16f5 to
6cb0843
Compare
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
On the Windows build, resizing the window showed visible artefacts: a white flash on open, and — most noticeably on large files — content (text, syntax highlighting, and the bottom status bar) that visibly trailed the window edge during a drag, failing to repaint fast enough to keep up.
Fix
Windows webview background_color matching the theme's --bg-main, so WebView2 paints a solid themed background immediately instead of white. Gated to #[cfg(windows)] — macOS ignores the API and Linux never had the flash.
Grow-only content freeze: while the window is growing, a .resizing class applies content-visibility: hidden to the content tree (~7× cheaper per frame, measured), freezing the last-painted frame; the newly-exposed area shows clean themed space and content snaps back ~120ms after the drag settles. Shrinking stays live — it has no trailing edge, and freezing a fast shrink flashed blank.
Removed the status bar's backdrop-filter — its background is opaque, so the blur had zero visual effect but cost a full-width blur pass every frame.
Minor: content-visibility: auto + accurate contain-intrinsic-size: 22px on line rows for smooth off-screen skipping.