fix(diff): let a drag cross the blank half of a split row

A split row pads whichever side of a change has no line, and that half
was drawn inert: no I-beam, a press that started nothing, and a range
that visibly stopped at the padding and resumed below it. A one-sided
change is the ordinary shape of a diff, so the dead band runs down most
of one column — the drag it swallows is the common one.

The blank half now carries the same press and move listeners as any
other cell and takes the selection colour when the range covers it. What
lands on the clipboard is unchanged: `DiffSelection::text` already reads
past the padding, because the blank half is layout, not a line.

Also drives an upward drag through the real entry points. `range()`
ordering was only asserted in `diff_rows`, where a selection is built by
hand; nothing checked that a drag whose head ends above its anchor
copies the same rows.
This commit is contained in:
l0ng-ai
2026-09-07 22:18:21 +08:00
parent 87dc26f061
commit f7fd1d43fd
+39 -1
View File
@@ -1383,7 +1383,19 @@ impl Tty7App {
Side::New => base.rounded_br(outer_radius),
};
let Some(cell) = cell else {
return base.bg(cx.theme().muted.opacity(0.3)).into_any_element();
// Blank, but still this row's half of this column. Left inert it
// is a dead band under the pointer — no I-beam, a press that
// starts nothing, and a range that visibly stops at the padding
// and resumes below it. A one-sided change is the ordinary shape
// of a diff, so that band runs down most of one column.
let fill = match rowsel.covers(id, Some(side)) {
true => cx.theme().selection,
false => cx.theme().muted.opacity(0.3),
};
return self
.diff_row_drag(base, rowsel, Some(side), id, cx)
.bg(fill)
.into_any_element();
};
let (marker, tint) = match (cell.changed, side) {
(true, Side::Old) => ("", Some(cx.theme().danger.opacity(0.12))),
@@ -2984,6 +2996,32 @@ mod gpui_tests {
assert_eq!(copied(&app, &mut vcx).as_deref(), Some("b\nc\nB"));
}
/// A drag that runs up the card leaves the head above the anchor. The
/// range is read in drawn order either way, so it copies what the same
/// two rows copy dragged the other way round.
#[gpui::test]
fn a_drag_up_a_column_copies_the_same_rows(cx: &mut TestAppContext) {
let (app, mut vcx) = window(cx);
drag(&app, &mut vcx, DiffViewMode::Split, Some(Side::New), 3, 0);
let reversed = app.update_in(&mut vcx, |this, _, _| {
let sel = this.tabs[0]
.diff_overlay
.as_ref()
.and_then(|o| o.selection.as_ref())
.expect("the drag this test just made");
sel.head < sel.anchor
});
assert!(reversed, "the drag ended above where it started");
assert_eq!(copied(&app, &mut vcx).as_deref(), Some("a\nB\nd"));
drag(&app, &mut vcx, DiffViewMode::Split, Some(Side::Old), 3, 0);
assert_eq!(copied(&app, &mut vcx).as_deref(), Some("a\nb\nc\nd"));
drag(&app, &mut vcx, DiffViewMode::Unified, None, 3, 1);
assert_eq!(copied(&app, &mut vcx).as_deref(), Some("b\nc\nB"));
}
/// The overlay is often drawn beside a live shell that holds the keyboard.
/// Ctrl+C is the copy key only once the overlay has taken focus — until
/// then the same keystroke would reach the pane and interrupt whatever is