feat: add pierre-style presentation to diff view

This commit is contained in:
2026-11-06 15:21:20 +07:00
parent 7214c70ea7
commit e8e1cb39e0
5 changed files with 636 additions and 229 deletions
+134 -38
View File
@@ -5,7 +5,7 @@ use gpui::{
AppContext as _, Context, Entity, IntoElement, MouseButton, ParentElement as _, Render,
Styled as _, TestAppContext, VisualTestContext, Window, div, point, px,
};
use gpui_native_editor::diff_view::{DiffRowKind, DiffViewState, diff_rows};
use gpui_native_editor::diff_view::{DiffRow, DiffRowKind, DiffViewState, diff_rows};
use gpui_native_editor::highlight::{Language, Role};
const OLD: &str = "fn a() {}\nfn b() {}\nfn c() {}\n";
@@ -26,6 +26,10 @@ fn rows_mark_removals_additions_and_context_with_line_numbers() {
DiffRowKind::Context,
]
);
assert!(
rows.iter().all(|row| row.kind != DiffRowKind::Hunk),
"changes within one context window need no separator"
);
let removed = &rows[1];
assert_eq!(removed.old_number, Some(2));
@@ -54,6 +58,64 @@ fn rows_mark_removals_additions_and_context_with_line_numbers() {
assert_eq!(context.new_number, Some(1));
}
#[test]
fn hunk_separators_carry_the_collapsed_line_count_and_header() {
// Ten lines with only line 6 changed: three lines of context trim the
// leading unchanged region to three hidden lines.
let mut old = String::new();
let mut new = String::new();
for line in 0..10 {
old.push_str(&format!("line {line}\n"));
if line == 6 {
new.push_str(&format!("line {line} changed\n"));
} else {
new.push_str(&format!("line {line}\n"));
}
}
let rows = diff_rows(&old, &new, Language::Rust);
let separators: Vec<&DiffRow> = rows
.iter()
.filter(|row| row.kind == DiffRowKind::Hunk)
.collect();
assert_eq!(separators.len(), 1, "one collapsed region above the hunk");
assert_eq!(separators[0].hidden_lines, 3);
assert_eq!(separators[0].header, "@@ -4,7 +4,7 @@");
let changed = rows
.iter()
.find(|row| row.kind == DiffRowKind::Removed)
.expect("the changed line renders as a removal");
assert_eq!(changed.old_number, Some(7));
}
#[test]
fn word_changes_tint_only_the_changed_words() {
let rows = diff_rows(OLD, NEW, Language::Rust);
let removed = &rows[1];
let removed_text: String = removed
.content
.spans
.iter()
.map(|span| span.text.as_ref())
.collect();
assert_eq!(removed.changed_ranges.len(), 1, "one word moved");
let range = &removed.changed_ranges[0];
assert_eq!(&removed_text[range.clone()], "b()");
let added = &rows[2];
let added_text: String = added
.content
.spans
.iter()
.map(|span| span.text.as_ref())
.collect();
assert_eq!(added.changed_ranges.len(), 1);
let range = &added.changed_ranges[0];
assert_eq!(&added_text[range.clone()], "x()");
}
#[test]
fn an_added_line_carries_its_highlight_spans() {
let rows = diff_rows("", "fn zed() {}\n", Language::Rust);
@@ -72,13 +134,6 @@ fn an_added_line_carries_its_highlight_spans() {
);
}
#[test]
fn identical_versions_are_all_context() {
let rows = diff_rows(OLD, OLD, Language::Rust);
assert_eq!(rows.len(), 3);
assert!(rows.iter().all(|row| row.kind == DiffRowKind::Context));
}
struct Host {
diff: Entity<DiffViewState>,
}
@@ -90,18 +145,17 @@ impl Render for Host {
}
fn diff_view_window(
rows: usize,
changes_at: usize,
lines: usize,
changed_lines: &[usize],
) -> (TestAppContext, VisualTestContext, Entity<DiffViewState>) {
let mut cx = TestAppContext::single();
cx.update(gpui_native_theme::Theme::init);
let mut old = String::new();
let mut new = String::new();
for line in 0..rows {
for line in 0..lines {
old.push_str(&format!("line {line} unchanged\n"));
if line == changes_at {
if changed_lines.contains(&line) {
new.push_str(&format!("line {line} rewritten\n"));
new.push_str(&format!("line {line} added\n"));
} else {
new.push_str(&format!("line {line} unchanged\n"));
}
@@ -124,17 +178,33 @@ fn diff_view_window(
#[test]
fn rows_render_virtualized_and_the_view_jumps_to_the_first_change() {
let (_cx, mut visual, state) = diff_view_window(200, 150);
let changed: Vec<usize> = (10..200).step_by(10).collect();
let (_cx, mut visual, state) = diff_view_window(200, &changed);
assert!(
visual.debug_bounds("test-diff-row-0").is_some(),
visual.debug_bounds("diff-row-0").is_some(),
"the first row renders"
);
assert!(
visual.debug_bounds("test-diff-row-199").is_none(),
visual.debug_bounds("diff-row-100").is_none(),
"rows past the viewport never render"
);
// Scroll to the bottom of the diff first, so the jump has somewhere to
// go.
visual.simulate_event(gpui::ScrollEvent {
position: point(px(500.), px(400.)),
delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-5000.))),
..Default::default()
});
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
assert!(
visual.debug_bounds("diff-row-0").is_none(),
"the view scrolled to the bottom"
);
state.update_in(&mut visual, |state, _window, _cx| {
state.scroll_to_first_change()
});
@@ -143,13 +213,23 @@ fn rows_render_virtualized_and_the_view_jumps_to_the_first_change() {
});
assert!(
visual.debug_bounds("test-diff-row-150").is_some(),
visual.debug_bounds("diff-row-4").is_some(),
"the first change scrolled into view"
);
assert!(
visual.debug_bounds("test-diff-row-0").is_none(),
"the context before it scrolled out"
visual.debug_bounds("diff-row-0").is_none(),
"the hunk header above it scrolled out"
);
assert!(
visual.debug_bounds("diff-row-100").is_none(),
"rows past the viewport still never render"
);
}
#[test]
fn identical_versions_produce_no_rows() {
let rows = diff_rows(OLD, OLD, Language::Rust);
assert!(rows.is_empty(), "identical versions have no diff to show");
}
#[test]
@@ -163,14 +243,8 @@ fn set_versions_re_diffs() {
});
});
let all_context = cx.update(|cx| {
state
.read(cx)
.rows()
.iter()
.all(|row| row.kind == DiffRowKind::Context)
});
assert!(all_context, "identical versions diff to context rows");
let empty = cx.update(|cx| state.read(cx).rows().is_empty());
assert!(empty, "identical versions have no diff to show");
}
#[test]
@@ -193,7 +267,7 @@ fn scrolling_horizontally_moves_rows_without_vertical_drift() {
});
let before = visual
.debug_bounds("test-diff-row-0")
.debug_bounds("diff-row-0")
.expect("the first row renders");
visual.simulate_event(gpui::ScrollEvent {
@@ -206,7 +280,7 @@ fn scrolling_horizontally_moves_rows_without_vertical_drift() {
});
let after = visual
.debug_bounds("test-diff-row-0")
.debug_bounds("diff-row-0")
.expect("the first row still renders");
assert!(
(after.origin.x - before.origin.x + px(200.)).abs() <= px(2.),
@@ -222,8 +296,19 @@ fn scrolling_horizontally_moves_rows_without_vertical_drift() {
fn scrolling_vertically_does_not_drift_horizontally() {
let mut cx = TestAppContext::single();
cx.update(gpui_native_theme::Theme::init);
let old = "fn same() {}\n".repeat(200);
let new = format!("{old}fn long() {{ {} }}\n", "text ".repeat(300));
// Spread changes keep the diff taller than the viewport, so there is
// something to scroll vertically; the long line forces a horizontal bar.
let mut old = String::new();
let mut body = String::new();
for line in 0..200 {
old.push_str(&format!("fn same_{line}() {{}}\n"));
if line % 10 == 0 {
body.push_str(&format!("fn changed_{line}() {{}}\n"));
} else {
body.push_str(&format!("fn same_{line}() {{}}\n"));
}
}
let new = format!("{body}fn long() {{ {} }}\n", "text ".repeat(300));
let state = DiffViewState::new("test-diff", old, new, Language::Rust);
let state_cell: Rc<RefCell<Option<Entity<DiffViewState>>>> = Rc::new(RefCell::new(None));
let cell = state_cell.clone();
@@ -238,7 +323,7 @@ fn scrolling_vertically_does_not_drift_horizontally() {
});
let before = visual
.debug_bounds("test-diff-row-0")
.debug_bounds("diff-row-0")
.expect("the first row renders");
// A pure vertical gesture...
@@ -259,7 +344,7 @@ fn scrolling_vertically_does_not_drift_horizontally() {
});
let after = visual
.debug_bounds("test-diff-row-0")
.debug_bounds("diff-row-0")
.expect("the first row still renders");
assert_eq!(
after.origin.x, before.origin.x,
@@ -275,8 +360,19 @@ fn scrolling_vertically_does_not_drift_horizontally() {
fn dragging_the_vertical_thumb_leaves_the_horizontal_offset_alone() {
let mut cx = TestAppContext::single();
cx.update(gpui_native_theme::Theme::init);
let old = "fn same() {}\n".repeat(200);
let new = format!("{old}fn long() {{ {} }}\n", "text ".repeat(300));
// Spread changes keep the diff taller than the viewport, so the vertical
// scrollbar exists; the long line forces the horizontal one.
let mut old = String::new();
let mut body = String::new();
for line in 0..200 {
old.push_str(&format!("fn same_{line}() {{}}\n"));
if line % 10 == 0 {
body.push_str(&format!("fn changed_{line}() {{}}\n"));
} else {
body.push_str(&format!("fn same_{line}() {{}}\n"));
}
}
let new = format!("{body}fn long() {{ {} }}\n", "text ".repeat(300));
let state = DiffViewState::new("test-diff", old, new, Language::Rust);
let state_cell: Rc<RefCell<Option<Entity<DiffViewState>>>> = Rc::new(RefCell::new(None));
let cell = state_cell.clone();
@@ -292,7 +388,7 @@ fn dragging_the_vertical_thumb_leaves_the_horizontal_offset_alone() {
// Scroll the strip left first, so horizontal drift would be observable.
let row = visual
.debug_bounds("test-diff-row-0")
.debug_bounds("diff-row-0")
.expect("the first row renders");
visual.simulate_event(gpui::ScrollEvent {
position: row.center(),
@@ -303,7 +399,7 @@ fn dragging_the_vertical_thumb_leaves_the_horizontal_offset_alone() {
window.draw(cx).clear(cx);
});
let before = visual
.debug_bounds("test-diff-row-0")
.debug_bounds("diff-row-0")
.expect("the first row still renders");
// Press the vertical thumb: right edge, within its minimum length.
@@ -336,7 +432,7 @@ fn dragging_the_vertical_thumb_leaves_the_horizontal_offset_alone() {
});
let after = visual
.debug_bounds("test-diff-row-0")
.debug_bounds("diff-row-0")
.expect("the first row still renders");
assert_eq!(
after.origin.x, before.origin.x,