You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the side-by-side diff view (:Gdiffsplit, and the Source Control panel's "open diff" flow from Session 197), the two panes line up at the top but visibly drift once you scroll past the first hunk. After a hunk with +3/-1, the HEAD pane ends up showing lines that are three rows higher than the working-copy pane shows for the "same" logical row. The longer you scroll, the worse it gets.
Expected
Both panes stay in visual lockstep — when the working-copy side shows an added line, the HEAD side shows a blank filler row, and vice versa. Vim/vimdiff and VSCode's diff view both do this. This should be backend-independent (TUI, GTK, Win-GUI).
Actual
Padding rows render correctly at the top (they're visible in the first hunk), but once scroll_top advances past that hunk, the two panes' rendered rows stop corresponding. Scrolling either pane drags the partner to a buffer line that doesn't match the partner's aligned row.
Why this is confusing to diagnose
The infrastructure is already in place — it's a subtle bug in the sync, not a missing feature:
Engine.diff_aligned: HashMap<WindowId, Vec<AlignedDiffEntry>> is populated by build_aligned_diff() (src/core/engine/mod.rs:3974) when a diff split is opened.
render.rs consumes it (line ~6748): when diff_aligned exists for a window, the render loop walks the aligned sequence and emits padding rows where source_line is None. Padding does render in both panes, and in isolation each pane shows the correct aligned view relative to its own scroll_top.
src/core/engine/search.rs::sync_scroll_binds() (line ~277) has a specific branch for the aligned-diff case (line 318 onwards) that maps the active pane's scroll_top → aligned index → partner's corresponding source_line.
Root cause (hypothesis)
sync_scroll_binds() sets partner.scroll_top to a buffer line number, but what it actually wants to sync is an aligned-row index. The conversion back to buffer line at lines ~336–353:
// Map that aligned index to the partner's buffer line.let partner_line = if target_idx < partner_aligned.len(){
partner_aligned[target_idx..].iter().find_map(|e| e.source_line)// fallback to previous real line if target is padding
...
};
w.view.scroll_top = partner_line;
When partner_aligned[target_idx] is a padding entry (source_line = None), the code walks forward to the next entry with a real source_line. That means:
Active pane: visible rows start at aligned row N (which may be a real line on active's side).
Partner pane: scroll_top is set to some later real buffer line; the partner render re-enters the aligned sequence at whatever aligned row that buffer line lives at — which is further along the aligned sequence than N. Net effect: the partner skips past the padding block instead of showing the padding.
Once one pane skips padding the other pane keeps, the two are permanently out of sync, and every subsequent hunk compounds the drift.
Fix sketch
Either:
Use aligned-row index for scroll, not buffer line. Treat scroll_top as an index into the aligned sequence for any window that has diff_aligned populated. The render loop already starts at the aligned index matching scroll_top; make the scroll unit match. This is a larger refactor because the view / cursor-movement code assumes scroll_top is a buffer line.
Cheaper targeted fix: in sync_scroll_binds, when partner_aligned[target_idx] is a padding entry, back up target_idx in the partner sequence to the start of the padding run (first padding entry at the current gap), then set partner.scroll_top to the buffer line of the aligned entry immediately before that run. The partner render's own "find aligned entry for scroll_top" step will then re-enter the sequence at the start of the padding block and emit the padding rows. Needs a corresponding adjustment in the render loop's initial aligned_idx calculation so that when scroll_top's aligned entry is followed by padding, we back up to the start of the padding (symmetric with the fix above).
Render-side fallback: change the aligned-index seek at render.rs:~6756 so that when multiple aligned entries match a given source_line (one real + preceding padding), it lands on the first (padding) entry. Combined with v0.3.0: TUI-only binary (vcd) #2, both sides consistently render padding at the right visual row.
Option #2 or #3 (or a combined minimal fix) should be enough; option #1 is the "clean" long-term refactor if this keeps causing surprises.
Acceptance
Open a file with multiple hunks (e.g. make several scattered edits to a tracked file).
Run :Gdiffsplit or use the SC panel's diff-open.
Scroll down past the first hunk on either side.
Both panes' rows correspond: added lines in one pane line up horizontally with blank filler in the other pane; deleted lines in one pane line up with blank filler in the other.
Drift of ≥1 row at any scroll offset is a failure.
Works in TUI and GTK; verify Win-GUI once they lands on the shared sync_scroll_binds path.
Reproduction
cd into a git repo, edit a file so it has 2+ non-adjacent hunks with mixed additions/deletions.
:Gdiffsplit (or click the file in the Source Control panel).
Observe first hunk lines up.
Scroll down (mouse wheel, j, or Ctrl-d) until past the first hunk.
Hunk 2 is misaligned horizontally by (net + − net -) of hunk 1.
Related code
src/core/engine/search.rs:277 — sync_scroll_binds (the likely culprit)
src/core/engine/mod.rs:3974 — build_aligned_diff
src/render.rs:~6748 — per-window render loop that emits padding from diff_aligned
Summary
In the side-by-side diff view (
:Gdiffsplit, and the Source Control panel's "open diff" flow from Session 197), the two panes line up at the top but visibly drift once you scroll past the first hunk. After a hunk with+3/-1, the HEAD pane ends up showing lines that are three rows higher than the working-copy pane shows for the "same" logical row. The longer you scroll, the worse it gets.Expected
Both panes stay in visual lockstep — when the working-copy side shows an added line, the HEAD side shows a blank filler row, and vice versa. Vim/
vimdiffand VSCode's diff view both do this. This should be backend-independent (TUI, GTK, Win-GUI).Actual
Padding rows render correctly at the top (they're visible in the first hunk), but once
scroll_topadvances past that hunk, the two panes' rendered rows stop corresponding. Scrolling either pane drags the partner to a buffer line that doesn't match the partner's aligned row.Why this is confusing to diagnose
The infrastructure is already in place — it's a subtle bug in the sync, not a missing feature:
Engine.diff_aligned: HashMap<WindowId, Vec<AlignedDiffEntry>>is populated bybuild_aligned_diff()(src/core/engine/mod.rs:3974) when a diff split is opened.render.rsconsumes it (line ~6748): whendiff_alignedexists for a window, the render loop walks the aligned sequence and emits padding rows wheresource_lineisNone. Padding does render in both panes, and in isolation each pane shows the correct aligned view relative to its ownscroll_top.src/core/engine/search.rs::sync_scroll_binds()(line ~277) has a specific branch for the aligned-diff case (line 318 onwards) that maps the active pane'sscroll_top→ aligned index → partner's correspondingsource_line.Root cause (hypothesis)
sync_scroll_binds()setspartner.scroll_topto a buffer line number, but what it actually wants to sync is an aligned-row index. The conversion back to buffer line at lines ~336–353:When
partner_aligned[target_idx]is a padding entry (source_line = None), the code walks forward to the next entry with a realsource_line. That means:scroll_topis set to some later real buffer line; the partner render re-enters the aligned sequence at whatever aligned row that buffer line lives at — which is further along the aligned sequence than N. Net effect: the partner skips past the padding block instead of showing the padding.Once one pane skips padding the other pane keeps, the two are permanently out of sync, and every subsequent hunk compounds the drift.
Fix sketch
Either:
Use aligned-row index for scroll, not buffer line. Treat
scroll_topas an index into the aligned sequence for any window that hasdiff_alignedpopulated. The render loop already starts at the aligned index matchingscroll_top; make the scroll unit match. This is a larger refactor because the view / cursor-movement code assumesscroll_topis a buffer line.Cheaper targeted fix: in
sync_scroll_binds, whenpartner_aligned[target_idx]is a padding entry, back uptarget_idxin the partner sequence to the start of the padding run (first padding entry at the current gap), then setpartner.scroll_topto the buffer line of the aligned entry immediately before that run. The partner render's own "find aligned entry for scroll_top" step will then re-enter the sequence at the start of the padding block and emit the padding rows. Needs a corresponding adjustment in the render loop's initialaligned_idxcalculation so that whenscroll_top's aligned entry is followed by padding, we back up to the start of the padding (symmetric with the fix above).Render-side fallback: change the aligned-index seek at
render.rs:~6756so that when multiple aligned entries match a givensource_line(one real + preceding padding), it lands on the first (padding) entry. Combined with v0.3.0: TUI-only binary (vcd) #2, both sides consistently render padding at the right visual row.Option #2 or #3 (or a combined minimal fix) should be enough; option #1 is the "clean" long-term refactor if this keeps causing surprises.
Acceptance
:Gdiffsplitor use the SC panel's diff-open.sync_scroll_bindspath.Reproduction
cdinto a git repo, edit a file so it has 2+ non-adjacent hunks with mixed additions/deletions.:Gdiffsplit(or click the file in the Source Control panel).j, orCtrl-d) until past the first hunk.+− net-) of hunk 1.Related code
src/core/engine/search.rs:277—sync_scroll_binds(the likely culprit)src/core/engine/mod.rs:3974—build_aligned_diffsrc/render.rs:~6748— per-window render loop that emits padding fromdiff_alignedsrc/core/engine/buffers.rs:2599—compute_diffpopulatesdiff_alignedsrc/core/engine/buffers.rs:1099—sc_apply_diff_splitregisters the scroll-bind pair