fix: synchronize horizontal scrolling between diff panes

This commit is contained in:
2026-10-11 14:50:36 +02:00
parent 165930fc1e
commit 7b4bc80538
3 changed files with 19 additions and 9 deletions
+1 -1
View File
@@ -255,7 +255,7 @@ In split diffs, changed text rows use muted red backgrounds in Before and muted
Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column. Unlike the text slash `/`, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched. Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings light diagonal) on the normal terminal background, one per column. Unlike the text slash `/`, this glyph is designed to reach cell corners so diagonals can join end to end across successive rows. Pixel-perfect joins depend on the terminal's font and line-spacing rendering. Actual empty source lines retain their normal/change background and are not hatched.
In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input pans only the hovered text pane, with independent Before/After offsets and no change to focus, cursor, or selections; over Files it is consumed. Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; **it never closes the diff**. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias. In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input in either split synchronizes the Before/After horizontal offsets, bounded by the longer side, without changing focus, cursor, or selections; over Files it is consumed. Added/deleted files retain single-pane scrolling. Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; **it never closes the diff**. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
Both diff panes apply available semantic tokens from their own buffer, independently of keyboard focus. Historical snapshots never borrow the working buffer's tokens; live After tokens must match the displayed text revision. Missing or invalidated tokens leave Tree-sitter syntax colors intact. Semantic colors and modifiers preserve the change backgrounds. Both diff panes apply available semantic tokens from their own buffer, independently of keyboard focus. Historical snapshots never borrow the working buffer's tokens; live After tokens must match the displayed text revision. Missing or invalidated tokens leave Tree-sitter syntax colors intact. Semantic colors and modifiers preserve the change backgrounds.
+14 -4
View File
@@ -615,6 +615,11 @@ impl DiffState {
Focus::After => self.max_after, Focus::After => self.max_after,
Focus::Files => return 0, Focus::Files => return 0,
}; };
let columns = if self.pane_layout() == PaneLayout::Split {
self.max_before.max(self.max_after)
} else {
columns
};
left.min(columns.saturating_add(1).saturating_sub(width.max(1))) left.min(columns.saturating_add(1).saturating_sub(width.max(1)))
} }
@@ -630,10 +635,15 @@ impl DiffState {
left.saturating_sub(3) left.saturating_sub(3)
}; };
let left = self.bound_horizontal(focus, left, width); let left = self.bound_horizontal(focus, left, width);
match focus { if self.pane_layout() == PaneLayout::Split {
Focus::Before => self.left_before = left, self.left_before = left;
Focus::After => self.left_after = left, self.left_after = left;
Focus::Files => unreachable!(), } else {
match focus {
Focus::Before => self.left_before = left,
Focus::After => self.left_after = left,
Focus::Files => unreachable!(),
}
} }
self.follow_caret = false; self.follow_caret = false;
} }
+4 -4
View File
@@ -4317,16 +4317,16 @@ mod tests {
.unwrap(); .unwrap();
assert!(screen_text(&terminal).contains("ore.before.")); assert!(screen_text(&terminal).contains("ore.before."));
assert_eq!(session.editor().diff().unwrap().left_before, 3); assert_eq!(session.editor().diff().unwrap().left_before, 3);
assert_eq!(session.editor().diff().unwrap().left_after, 0); assert_eq!(session.editor().diff().unwrap().left_after, 3);
let mut shift = document_wheel(MouseEventKind::ScrollDown, after.x + 1, after.y + 1); let mut shift = document_wheel(MouseEventKind::ScrollDown, after.x + 1, after.y + 1);
shift.modifiers = KeyModifiers::SHIFT; shift.modifiers = KeyModifiers::SHIFT;
assert!(viewports.scroll(&mut session, shift)); assert!(viewports.scroll(&mut session, shift));
terminal terminal
.draw(|frame| viewports.draw_session(frame, &mut session)) .draw(|frame| viewports.draw_session(frame, &mut session))
.unwrap(); .unwrap();
assert!(screen_text(&terminal).contains("er.after.")); assert!(screen_text(&terminal).contains("after.after."));
assert_eq!(session.editor().diff().unwrap().left_before, 3); assert_eq!(session.editor().diff().unwrap().left_before, 6);
assert_eq!(session.editor().diff().unwrap().left_after, 3); assert_eq!(session.editor().diff().unwrap().left_after, 6);
assert_eq!(session.editor().diff().unwrap().top, 0); assert_eq!(session.editor().diff().unwrap().top, 0);
assert_eq!(session.editor().diff().unwrap().focus, initial_focus); assert_eq!(session.editor().diff().unwrap().focus, initial_focus);
assert_eq!(session.editor().cursor(), initial_cursor); assert_eq!(session.editor().cursor(), initial_cursor);