fix(diff): a new or deleted file draws unified in the split view (#1138)

A new or deleted file has one side only, so in the split view half its card was empty. Such a file now draws unified whatever the view, and the others still split. A drag takes its mode from the row it starts on, so selecting and copying in one of these files works in either view. `file_mode` in `diff_list.rs` picks the mode, with tests; `cargo test --bin tty7-app diff` gives 90 passed, 1 ignored.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Adam Hitchcock
2026-10-09 10:49:39 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8229c28298
commit 45e5bde4ed
2 changed files with 65 additions and 15 deletions
+38
View File
@@ -155,7 +155,17 @@ pub(crate) fn preview_rows(file: &FileDiff, mode: DiffViewMode) -> Vec<DiffRow>
file_rows(usize::MAX, file, true, mode)
}
/// The view `file` draws in: a new or deleted file has one side only, so
/// split would be half empty.
pub(crate) fn file_mode(file: &FileDiff, mode: DiffViewMode) -> DiffViewMode {
match file.status {
FileStatus::Added | FileStatus::Deleted => DiffViewMode::Unified,
_ => mode,
}
}
fn file_rows(index: usize, file: &FileDiff, expanded: bool, mode: DiffViewMode) -> Vec<DiffRow> {
let mode = file_mode(file, mode);
let expandable =
!file.binary && (!file.hunks.is_empty() || file.truncated == Some(Truncation::Budget));
let shown_path = match &file.old_path {
@@ -382,6 +392,34 @@ mod tests {
);
}
/// A new or deleted file has nothing to set beside it, so split draws it
/// unified; a modified file in the same list still splits.
#[test]
fn split_draws_a_new_or_deleted_file_unified() {
let added = FileDiff {
status: FileStatus::Added,
..file("new.rs", 1)
};
let deleted = FileDiff {
status: FileStatus::Deleted,
..file("gone.rs", 1)
};
for one_sided in [added, deleted] {
let snap = snapshot(vec![one_sided, file("a.rs", 1)]);
let rows = build_rows(&snap, &open(["new.rs"]), None, DiffViewMode::Split, false);
let kinds: Vec<_> = shape(&rows)
.into_iter()
.filter(|k| matches!(*k, "split" | "unified"))
.collect();
assert_eq!(
kinds,
[
"unified", "unified", "unified", "unified", "split", "split", "split"
]
);
}
}
#[test]
fn only_the_first_hunk_of_a_file_follows_its_header() {
let rows = build_rows(
+27 -15
View File
@@ -1046,16 +1046,6 @@ impl Tty7App {
let active = self.active;
let overlay = self.tabs.get_mut(active)?.diff_overlay.as_mut()?;
// Forget a range the rows under it no longer answer to. The two views
// pair the same lines differently, so a row range drawn in one of them
// points at other code in the other. Here rather than beside the
// switch that flips the mode: this is the one place that knows which
// rows are about to be drawn.
if overlay.selection.as_ref().is_some_and(|s| s.mode != mode) {
overlay.selection = None;
overlay.selecting = false;
}
let snap = match &overlay.load {
DiffLoad::Loading => return Some(DiffBody::Message(t(L10nKey::DiffReading))),
DiffLoad::NotARepo => return Some(DiffBody::Message(t(L10nKey::DiffNotARepo))),
@@ -1081,6 +1071,27 @@ impl Tty7App {
None => None,
};
// Forget a range the rows under it no longer answer to. The two views
// pair the same lines differently, so a row range drawn in one of them
// points at other code in the other. Here rather than beside the
// switch that flips the mode: this is the one place that knows which
// rows are about to be drawn.
let drawn = |path: &str| {
preview
.as_deref()
.filter(|f| f.path == path)
.or_else(|| snap.files.iter().find(|f| f.path == path))
.map_or(mode, |f| crate::ui::diff_list::file_mode(f, mode))
};
if overlay
.selection
.as_ref()
.is_some_and(|s| s.mode != drawn(&s.path))
{
overlay.selection = None;
overlay.selecting = false;
}
let focused = focused_file(&snap, overlay);
let from = RowsFrom {
snap: &snap,
@@ -1135,7 +1146,6 @@ impl Tty7App {
let drag = Drag {
sel: overlay.selection.clone().map(Rc::new),
selecting: overlay.selecting,
mode: view_mode(cx),
};
let body = gpui::list(list.clone(), move |ix, _window, cx| {
#[cfg(test)]
@@ -1197,9 +1207,6 @@ struct Drag {
/// while one is: a diff runs to thousands of rows, and a listener each is
/// worth paying for during a drag and not otherwise.
selecting: bool,
/// The view the rows on screen are drawn in, which is the view a press
/// starts its selection in.
mode: DiffViewMode,
}
impl Drag {
@@ -1535,7 +1542,12 @@ fn diff_row_drag<E: InteractiveElement + Styled>(
drag: &Drag,
app: &gpui::WeakEntity<Tty7App>,
) -> E {
let mode = drag.mode;
// From the row, not the overlay's view: a new or deleted file draws
// unified whatever the view (`diff_list::file_mode`).
let mode = match side {
Some(_) => DiffViewMode::Split,
None => DiffViewMode::Unified,
};
// The I-beam is the only standing sign that this text can be taken;
// nothing else about a row says so until one is dragged.
let el = el.cursor_text().on_mouse_down(MouseButton::Left, {