feat: add diff view component

This commit is contained in:
2026-11-06 08:01:23 +07:00
parent f272f80d09
commit 7214c70ea7
11 changed files with 1067 additions and 35 deletions
+3
View File
@@ -10,6 +10,7 @@ gpui-native-core = { workspace = true, optional = true }
gpui-native-theme = { workspace = true }
gpui-native-utils = { workspace = true }
tree-sitter = "0.25"
similar = "2"
tree-sitter-rust = { version = "0.24", optional = true }
tree-sitter-typescript = { version = "0.23", optional = true }
tree-sitter-javascript = { version = "0.23", optional = true }
@@ -22,6 +23,7 @@ gpui = { workspace = true, features = ["test-support"] }
default = ["full"]
full = [
"code_view",
"diff_view",
"rust",
"typescript",
"javascript",
@@ -32,6 +34,7 @@ code_view = [
"gpui-native-core/scroll_area",
"gpui-native-core/scrollbar",
]
diff_view = ["code_view"]
rust = ["dep:tree-sitter-rust"]
typescript = ["dep:tree-sitter-typescript"]
javascript = ["dep:tree-sitter-javascript"]
+3 -3
View File
@@ -287,7 +287,7 @@ impl Render for CodeViewState {
let selection = self.selection();
let gutter_chars = line_count.max(1).to_string().len().max(2);
let gutter_width = text_size * 0.62 * gutter_chars as f32 + text_size;
let gutter_width = text_size * 0.62 * (gutter_chars as f32 + 1.0);
let bounds_cell: std::rc::Rc<std::cell::RefCell<Option<Bounds<Pixels>>>> =
std::rc::Rc::new(std::cell::RefCell::new(None));
@@ -611,7 +611,7 @@ fn run_style(role: Role, selected: bool, colors: ColorTokens, ramps: Ramps) -> H
}
/// A gutter cell sized to the whole row.
fn gutter_cell<E>(
pub(crate) fn gutter_cell<E>(
width: Pixels,
height: Pixels,
mono_font: &'static str,
@@ -637,7 +637,7 @@ where
}
/// Maps a highlight role to the theme's semantic palette.
fn role_color(role: Role, colors: ColorTokens, ramps: Ramps) -> Hsla {
pub(crate) fn role_color(role: Role, colors: ColorTokens, ramps: Ramps) -> Hsla {
match role {
Role::Text => colors.foreground,
Role::Keyword => ramps.primary.base,
+353
View File
@@ -0,0 +1,353 @@
//! The diff view: a unified, syntax-highlighted rendering of two versions of
//! a file. Rows reuse the code view's highlighting; consumers that already
//! hold a git diff can build [`DiffRow`]s themselves instead of calling
//! [`diff_rows`].
use gpui::prelude::FluentBuilder as _;
use gpui::{
Context, ElementId, HighlightStyle, Hsla, InteractiveElement as _, IntoElement,
ListHorizontalSizingBehavior, ParentElement as _, Pixels, Render, ScrollStrategy, SharedString,
StatefulInteractiveElement as _, Styled as _, TextStyle, UniformListScrollHandle, Window, div,
point, uniform_list,
};
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
use similar::{ChangeTag, TextDiff};
use crate::code_view::{gutter_cell, role_color};
use crate::highlight::{Language, Line, Role, highlight};
/// What a diff row did to the file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiffRowKind {
/// Present in both versions.
Context,
/// Only in the new version.
Added,
/// Only in the old version.
Removed,
}
/// One row of a unified diff.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffRow {
pub kind: DiffRowKind,
/// The highlighted line content.
pub content: Line,
/// 1-based line number in the old version, when the row exists there.
pub old_number: Option<usize>,
/// 1-based line number in the new version, when the row exists there.
pub new_number: Option<usize>,
}
impl DiffRow {
/// The row's text length, in characters.
fn char_count(&self) -> usize {
self.content
.spans
.iter()
.map(|span| span.text.chars().count())
.sum()
}
}
/// Computes unified rows between two texts, highlighting every line with `language`.
pub fn diff_rows(old: &str, new: &str, language: Language) -> Vec<DiffRow> {
let old_code = highlight(old, language);
let new_code = highlight(new, language);
let diff = TextDiff::from_lines(old, new);
let mut rows = Vec::new();
for change in diff.iter_all_changes() {
let code = match change.tag() {
ChangeTag::Equal => change.new_index().and_then(|ix| new_code.lines.get(ix)),
ChangeTag::Delete => change.old_index().and_then(|ix| old_code.lines.get(ix)),
ChangeTag::Insert => change.new_index().and_then(|ix| new_code.lines.get(ix)),
};
rows.push(DiffRow {
kind: match change.tag() {
ChangeTag::Equal => DiffRowKind::Context,
ChangeTag::Delete => DiffRowKind::Removed,
ChangeTag::Insert => DiffRowKind::Added,
},
content: code.cloned().unwrap_or_default(),
old_number: change.old_index().map(|ix| ix + 1),
new_number: change.new_index().map(|ix| ix + 1),
});
}
rows
}
/// Retained state for a diff view.
pub struct DiffViewState {
id: ElementId,
rows: Vec<DiffRow>,
/// Drives both scroll axes of the virtualized row list.
handle: UniformListScrollHandle,
a11y_label: Option<SharedString>,
}
impl DiffViewState {
/// Builds the state, diffing `old` against `new`.
pub fn new<I, O, N>(id: I, old: O, new: N, language: Language) -> Self
where
I: Into<ElementId>,
O: AsRef<str>,
N: AsRef<str>,
{
Self {
id: id.into(),
rows: diff_rows(old.as_ref(), new.as_ref(), language),
handle: UniformListScrollHandle::new(),
a11y_label: None,
}
}
/// Replaces the versions and re-diffs them.
pub fn set_versions<O, N>(&mut self, old: O, new: N, language: Language, cx: &mut Context<Self>)
where
O: AsRef<str>,
N: AsRef<str>,
{
self.rows = diff_rows(old.as_ref(), new.as_ref(), language);
self.handle.scroll_to_item(0, ScrollStrategy::Top);
// The replaced versions rarely share the old width; start at the left.
let offset = ScrollbarHandle::offset(&self.handle);
ScrollbarHandle::set_offset(&self.handle, point(Pixels::ZERO, offset.y));
cx.notify();
}
/// The diff rows.
pub fn rows(&self) -> &[DiffRow] {
&self.rows
}
/// Scrolls the first non-context row to the top of the view.
pub fn scroll_to_first_change(&self) {
let Some(row) = self
.rows
.iter()
.position(|row| row.kind != DiffRowKind::Context)
else {
return;
};
self.handle.scroll_to_item(row, ScrollStrategy::Top);
}
/// Overrides the accessibility label of the view.
pub fn accessibility_label<L>(mut self, label: L) -> Self
where
L: Into<SharedString>,
{
self.a11y_label = Some(label.into());
self
}
}
impl Render for DiffViewState {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let text_size = theme.sizes.small.text_size;
let line_height = text_size * 1.5;
let id = self.id.clone();
let state = cx.entity();
let row_count = self.rows.len();
let widest_number = self
.rows
.iter()
.flat_map(|row| [row.old_number, row.new_number])
.flatten()
.max()
.unwrap_or(1);
let number_chars = widest_number.to_string().len().max(2);
let number_width = text_size * 0.62 * (number_chars as f32 + 1.0);
let marker_width = text_size;
let char_width = text_size * 0.62;
let widest_row = self
.rows
.iter()
.enumerate()
.max_by_key(|(_, row)| row.char_count())
.map(|(ix, _)| ix);
let mut rows = uniform_list((id.clone(), "rows"), row_count, {
let id = id.clone();
move |range, _window, cx| {
let diff = state.read(cx);
let theme = cx.theme();
range
.map(|row_ix| {
render_row(
&diff.rows[row_ix],
&id,
row_ix,
theme.mono_font,
theme.colors,
theme.ramps,
theme.sizes.small.text_size,
line_height,
char_width,
marker_width,
number_width,
)
})
.collect()
}
})
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
.with_width_from_item(widest_row)
.track_scroll(&self.handle)
.h_full();
// Lock each gesture to its initial axis
rows.interactivity().base_style.restrict_scroll_to_axis = Some(true);
div()
.id((id.clone(), "view"))
.relative()
.size_full()
.text_size(text_size)
.when_some(self.a11y_label.clone(), |view, label| {
view.aria_label(label)
})
.child(rows)
.child(
Scrollbar::vertical(self.handle.clone())
.key("v-scrollbar")
.visibility(ScrollbarVisibility::Platform),
)
.child(
Scrollbar::horizontal(self.handle.clone())
.key("h-scrollbar")
.visibility(ScrollbarVisibility::Platform),
)
}
}
#[allow(clippy::too_many_arguments)]
fn render_row(
row: &DiffRow,
id: &ElementId,
row_ix: usize,
mono_font: &'static str,
colors: ColorTokens,
ramps: Ramps,
text_size: Pixels,
line_height: Pixels,
char_width: Pixels,
marker_width: Pixels,
number_width: Pixels,
) -> gpui::AnyElement {
let wash = match row.kind {
DiffRowKind::Context => None,
DiffRowKind::Added => Some(wash(ramps.success.base)),
DiffRowKind::Removed => Some(wash(ramps.destructive.base)),
};
let marker_color = match row.kind {
DiffRowKind::Added => Some(ramps.success.base),
DiffRowKind::Removed => Some(ramps.destructive.base),
DiffRowKind::Context => None,
};
let marker = match row.kind {
DiffRowKind::Added => "+",
DiffRowKind::Removed => "-",
DiffRowKind::Context => "",
};
let mut text = String::new();
let mut highlights: Vec<(std::ops::Range<usize>, HighlightStyle)> = Vec::new();
for span in &row.content.spans {
let start = text.len();
text.push_str(&span.text);
if span.role != Role::Text {
highlights.push((
start..text.len(),
HighlightStyle {
color: Some(role_color(span.role, colors, ramps)),
..Default::default()
},
));
}
}
let style = TextStyle {
color: colors.foreground,
font_family: SharedString::from(mono_font),
font_size: text_size.into(),
line_height: line_height.into(),
..Default::default()
};
div()
.debug_selector({
let name = format!("{id}-row-{row_ix}");
move || name
})
.id(("diff-row", row_ix))
.h(line_height)
.flex()
.items_center()
.overflow_hidden()
.when_some(wash, |row_element, wash| row_element.bg(wash))
.child(gutter_cell(
marker_width,
line_height,
mono_font,
text_size,
marker_color.unwrap_or(gpui::transparent_black()),
marker,
))
.child(
gutter_cell(
number_width,
line_height,
mono_font,
text_size,
colors.muted_foreground,
row.old_number
.map(|number| number.to_string())
.unwrap_or_default(),
)
.justify_end()
.whitespace_nowrap(),
)
.child(
gutter_cell(
number_width,
line_height,
mono_font,
text_size,
colors.muted_foreground,
row.new_number
.map(|number| number.to_string())
.unwrap_or_default(),
)
.justify_end()
.whitespace_nowrap(),
)
.child(
div()
.flex_none()
.w(char_width * row.char_count() as f32)
.whitespace_nowrap()
.child(gpui::StyledText::new(text).with_default_highlights(&style, highlights)),
)
.into_any_element()
}
/// A change wash: the ramp color at the alpha rows tint with.
fn wash(color: Hsla) -> Hsla {
Hsla {
a: color.a * 0.14,
..color
}
}
+3
View File
@@ -5,4 +5,7 @@
#[cfg(feature = "code_view")]
pub mod code_view;
#[cfg(feature = "diff_view")]
pub mod diff_view;
pub mod highlight;
+349
View File
@@ -0,0 +1,349 @@
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<DiffRowKind> = 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<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(
rows: usize,
changes_at: 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 {
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<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 (_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<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("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<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("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<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("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"
);
}
+109
View File
@@ -0,0 +1,109 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{Duration, Instant};
use gpui::{
AppContext as _, Context, Entity, IntoElement, Modifiers, ParentElement as _, Render,
ScrollDelta, ScrollEvent, Styled as _, TestAppContext, VisualTestContext, Window, div, point,
px,
};
use gpui_native_editor::diff_view::DiffViewState;
/// Deterministic old/new pairs of roughly `lines` lines each, with changes
/// sprinkled through.
fn large_diff_pair(lines: usize) -> (String, String) {
let mut old = String::with_capacity(lines * 48);
let mut new = String::with_capacity(lines * 48);
let mut index = 0usize;
while old.lines().count() < lines {
let i = index;
let comment = format!("// Handler {i} in the generated dispatch table.\n");
old.push_str(&comment);
new.push_str(&comment);
old.push_str(&format!("fn handle_{i}(payload: &str) -> bool {{\n"));
new.push_str(&format!("fn handle_{i}(payload: &Payload) -> bool {{\n"));
old.push_str(&format!(" payload.len() < {i} + 16\n"));
new.push_str(&format!(
" payload.size() < {i} + 16 && !payload.is_empty()\n"
));
if i.is_multiple_of(5) {
old.push_str(&format!(" log::debug(\"handler {i} ran\");\n"));
}
if i.is_multiple_of(7) {
new.push_str(" metrics::count(\"handle\");\n");
}
old.push_str("}\n\n");
new.push_str("}\n\n");
index += 1;
}
(old, new)
}
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())
}
}
#[test]
fn scrolling_a_large_diff_is_not_pathologically_slow() {
let mut cx = TestAppContext::single();
cx.update(gpui_native_theme::Theme::init);
let (old, new) = large_diff_pair(5000);
let started = Instant::now();
let state = DiffViewState::new(
"bench-diff",
&old,
&new,
gpui_native_editor::highlight::Language::Rust,
);
let diff_elapsed = started.elapsed();
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);
});
let row_count = cx.update(|cx| state.read(cx).rows().len());
assert!(row_count > 5000, "changes add rows on top of the context");
let started = Instant::now();
for _ in 0..30 {
visual.simulate_event(ScrollEvent {
position: point(px(300.), px(300.)),
delta: ScrollDelta::Pixels(point(px(0.), px(-240.))),
modifiers: Modifiers::default(),
..Default::default()
});
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
}
let scroll_elapsed = started.elapsed();
println!(
"diffing and highlighting into {row_count} rows took {diff_elapsed:?}; 30 wheel frames took {scroll_elapsed:?} ({:?} per frame)",
scroll_elapsed / 30
);
assert!(
diff_elapsed < Duration::from_secs(3),
"diffing took {diff_elapsed:?}"
);
assert!(
scroll_elapsed < Duration::from_secs(2),
"scrolling 30 frames took {scroll_elapsed:?}"
);
}