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::{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 = rows.iter().map(|row| row.kind).collect(); assert_eq!( kinds, vec![ DiffRowKind::Context, DiffRowKind::Removed, DiffRowKind::Added, DiffRowKind::Added, DiffRowKind::Context, ] ); 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 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" ); } #[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, } impl Render for Host { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div().size_full().child(self.diff.clone()) } } fn diff_view_window( rows: usize, changes_at: usize, ) -> (TestAppContext, VisualTestContext, Entity) { 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 { old.push_str(&format!("line {line} unchanged\n")); if line == changes_at { 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")); } } let state = DiffViewState::new("test-diff", old, new, Language::Rust); let state_cell: Rc>>> = 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 (_cx, mut visual, state) = diff_view_window(200, 150); assert!( visual.debug_bounds("test-diff-row-0").is_some(), "the first row renders" ); assert!( visual.debug_bounds("test-diff-row-199").is_none(), "rows past the viewport never render" ); 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("test-diff-row-150").is_some(), "the first change scrolled into view" ); assert!( visual.debug_bounds("test-diff-row-0").is_none(), "the context before it scrolled out" ); } #[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 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"); } #[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>>> = 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("test-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("test-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); let old = "fn same() {}\n".repeat(200); let new = format!("{old}fn long() {{ {} }}\n", "text ".repeat(300)); let state = DiffViewState::new("test-diff", old, new, Language::Rust); let state_cell: Rc>>> = 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("test-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("test-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); let old = "fn same() {}\n".repeat(200); let new = format!("{old}fn long() {{ {} }}\n", "text ".repeat(300)); let state = DiffViewState::new("test-diff", old, new, Language::Rust); let state_cell: Rc>>> = 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("test-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("test-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("test-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" ); }