Files
gpui-native-kit/crates/editor/tests/diff_view.rs
T
reya b07b1d6c2e
CI / check (pull_request) Waiting to run
clean up
2026-11-08 13:26:21 +07:00

440 lines
14 KiB
Rust

use std::cell::RefCell;
use std::rc::Rc;
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::{DiffRow, DiffRowKind, DiffViewState, diff_rows};
use gpui_native_editor::highlight::{Language, Role};
const OLD: &str = "fn a() {}\nfn b() {}\nfn c() {}\n";
const NEW: &str = "fn a() {}\nfn x() {}\nfn y() {}\nfn c() {}\n";
#[test]
fn rows_mark_removals_additions_and_context_with_line_numbers() {
let rows = diff_rows(OLD, NEW, Language::Rust);
let kinds: Vec<DiffRowKind> = rows.iter().map(|row| row.kind).collect();
assert_eq!(
kinds,
vec![
DiffRowKind::Context,
DiffRowKind::Removed,
DiffRowKind::Added,
DiffRowKind::Added,
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));
assert_eq!(removed.new_number, None);
let removed_text: String = removed
.content
.spans
.iter()
.map(|span| span.text.as_ref())
.collect();
assert!(removed_text.contains("fn b() {}"));
let added = &rows[2];
assert_eq!(added.old_number, None);
assert_eq!(added.new_number, Some(2));
let added_text: String = added
.content
.spans
.iter()
.map(|span| span.text.as_ref())
.collect();
assert!(added_text.contains("fn x() {}"));
let context = &rows[0];
assert_eq!(context.old_number, Some(1));
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);
let added = rows
.iter()
.find(|row| row.kind == DiffRowKind::Added)
.expect("the new file is all additions");
assert!(
added
.content
.spans
.iter()
.any(|span| span.role == Role::Keyword),
"fn highlights as a keyword inside the diff row"
);
}
struct Host {
diff: Entity<DiffViewState>,
}
impl Render for Host {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(self.diff.clone())
}
}
fn diff_view_window(
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..lines {
old.push_str(&format!("line {line} unchanged\n"));
if changed_lines.contains(&line) {
new.push_str(&format!("line {line} rewritten\n"));
} else {
new.push_str(&format!("line {line} unchanged\n"));
}
}
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();
let window = cx.add_window(move |_, cx| {
let diff = cx.new(|_| state);
cell.borrow_mut().replace(diff.clone());
Host { diff }
});
let state = state_cell.borrow().clone().expect("state captured");
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
(cx, visual, state)
}
#[test]
fn rows_render_virtualized_and_the_view_jumps_to_the_first_change() {
let changed: Vec<usize> = (10..200).step_by(10).collect();
let (_cx, mut visual, state) = diff_view_window(200, &changed);
assert!(
visual.debug_bounds("diff-row-0").is_some(),
"the first row renders"
);
assert!(
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()
});
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
assert!(
visual.debug_bounds("diff-row-4").is_some(),
"the first change scrolled into view"
);
assert!(
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 set_versions_re_diffs() {
let mut cx = TestAppContext::single();
let state = cx.new(|_| DiffViewState::new("test-diff", OLD, NEW, Language::Rust));
cx.update(|cx| {
state.update(cx, |state, cx| {
state.set_versions("same\n", "same\n", Language::Rust, cx)
});
});
let empty = cx.update(|cx| state.read(cx).rows().is_empty());
assert!(empty, "identical versions have no diff to show");
}
#[test]
fn scrolling_horizontally_moves_rows_without_vertical_drift() {
let mut cx = TestAppContext::single();
cx.update(gpui_native_theme::Theme::init);
let old = "fn same() {}\n".repeat(40);
let new = format!("{old}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();
let window = cx.add_window(move |_, cx| {
let diff = cx.new(|_| state);
cell.borrow_mut().replace(diff.clone());
Host { diff }
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
let before = visual
.debug_bounds("diff-row-0")
.expect("the first row renders");
visual.simulate_event(gpui::ScrollEvent {
position: before.center(),
delta: gpui::ScrollDelta::Pixels(point(px(-200.), px(0.))),
..Default::default()
});
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
let after = visual
.debug_bounds("diff-row-0")
.expect("the first row still renders");
assert!(
(after.origin.x - before.origin.x + px(200.)).abs() <= px(2.),
"a horizontal wheel gesture scrolls the rows left"
);
assert_eq!(
after.origin.y, before.origin.y,
"a horizontal gesture must not drift vertically"
);
}
#[test]
fn scrolling_vertically_does_not_drift_horizontally() {
let mut cx = TestAppContext::single();
cx.update(gpui_native_theme::Theme::init);
// 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();
let window = cx.add_window(move |_, cx| {
let diff = cx.new(|_| state);
cell.borrow_mut().replace(diff.clone());
Host { diff }
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
let before = visual
.debug_bounds("diff-row-0")
.expect("the first row renders");
// A pure vertical gesture...
visual.simulate_event(gpui::ScrollEvent {
position: before.center(),
delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-5.))),
..Default::default()
});
// ...then a vertical one carrying minor horizontal noise, below the
// threshold where gpui would unlock the gesture's axis.
visual.simulate_event(gpui::ScrollEvent {
position: before.center(),
delta: gpui::ScrollDelta::Pixels(point(px(2.), px(-5.))),
..Default::default()
});
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
let after = visual
.debug_bounds("diff-row-0")
.expect("the first row still renders");
assert_eq!(
after.origin.x, before.origin.x,
"vertical gestures with horizontal noise must not scroll horizontally"
);
assert!(
(after.origin.y - (before.origin.y - px(10.))).abs() <= px(2.),
"the vertical component scrolls the rows up"
);
}
#[test]
fn dragging_the_vertical_thumb_leaves_the_horizontal_offset_alone() {
let mut cx = TestAppContext::single();
cx.update(gpui_native_theme::Theme::init);
// 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();
let window = cx.add_window(move |_, cx| {
let diff = cx.new(|_| state);
cell.borrow_mut().replace(diff.clone());
Host { diff }
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
// Scroll the strip left first, so horizontal drift would be observable.
let row = visual
.debug_bounds("diff-row-0")
.expect("the first row renders");
visual.simulate_event(gpui::ScrollEvent {
position: row.center(),
delta: gpui::ScrollDelta::Pixels(point(px(-100.), px(0.))),
..Default::default()
});
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
let before = visual
.debug_bounds("diff-row-0")
.expect("the first row still renders");
// Press the vertical thumb: right edge, within its minimum length.
let (thumb_width, inset, min_thumb) = cx.update(|cx| {
let theme = gpui_native_theme::Theme::global(cx);
(
theme.scrollbar.width,
theme.scrollbar.inset,
theme.scrollbar.min_thumb,
)
});
let press = point(
before.origin.x + before.size.width - thumb_width / 2.,
before.origin.y + inset + min_thumb / 2.,
);
visual.simulate_event(gpui::MouseDownEvent {
position: press,
button: MouseButton::Left,
..Default::default()
});
// Move sideways while held; only the vertical thumb is being dragged.
visual.simulate_event(gpui::MouseMoveEvent {
position: press + point(px(30.), px(0.)),
pressed_button: Some(MouseButton::Left),
..Default::default()
});
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
let after = visual
.debug_bounds("diff-row-0")
.expect("the first row still renders");
assert_eq!(
after.origin.x, before.origin.x,
"dragging the vertical thumb must not scroll the strip horizontally"
);
assert_eq!(
after.origin.y, before.origin.y,
"a sideways-only drag must not scroll vertically"
);
}