feat: add pierre-style presentation to diff view
This commit is contained in:
@@ -14,6 +14,7 @@ use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibilit
|
|||||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
||||||
|
|
||||||
use crate::highlight::{HighlightedCode, Language, Role, highlight_with_tree};
|
use crate::highlight::{HighlightedCode, Language, Role, highlight_with_tree};
|
||||||
|
use crate::utils::{gutter_cell, run_style};
|
||||||
|
|
||||||
actions!(code_view, [Copy]);
|
actions!(code_view, [Copy]);
|
||||||
|
|
||||||
@@ -285,9 +286,7 @@ impl Render for CodeViewState {
|
|||||||
let state = cx.entity();
|
let state = cx.entity();
|
||||||
let line_count = self.code.lines.len();
|
let line_count = self.code.lines.len();
|
||||||
let selection = self.selection();
|
let selection = self.selection();
|
||||||
|
let gutter_width = root_font_size * 2.5;
|
||||||
let gutter_chars = line_count.max(1).to_string().len().max(2);
|
|
||||||
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>>>> =
|
let bounds_cell: std::rc::Rc<std::cell::RefCell<Option<Bounds<Pixels>>>> =
|
||||||
std::rc::Rc::new(std::cell::RefCell::new(None));
|
std::rc::Rc::new(std::cell::RefCell::new(None));
|
||||||
@@ -313,7 +312,7 @@ impl Render for CodeViewState {
|
|||||||
theme.ramps,
|
theme.ramps,
|
||||||
theme.sizes.small.text_size,
|
theme.sizes.small.text_size,
|
||||||
line_height,
|
line_height,
|
||||||
gutter_width,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -474,7 +473,7 @@ fn render_row(
|
|||||||
ramps: Ramps,
|
ramps: Ramps,
|
||||||
text_size: Pixels,
|
text_size: Pixels,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
gutter_width: Pixels,
|
cx: &App,
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
let line = code.code().lines.get(line_ix).cloned().unwrap_or_default();
|
let line = code.code().lines.get(line_ix).cloned().unwrap_or_default();
|
||||||
|
|
||||||
@@ -575,10 +574,7 @@ fn render_row(
|
|||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
gutter_cell(
|
gutter_cell(
|
||||||
gutter_width,
|
cx,
|
||||||
line_height,
|
|
||||||
mono_font,
|
|
||||||
text_size,
|
|
||||||
if active {
|
if active {
|
||||||
colors.foreground
|
colors.foreground
|
||||||
} else {
|
} else {
|
||||||
@@ -601,52 +597,3 @@ fn render_row(
|
|||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_style(role: Role, selected: bool, colors: ColorTokens, ramps: Ramps) -> HighlightStyle {
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(role_color(role, colors, ramps)),
|
|
||||||
background_color: selected.then_some(colors.selection),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A gutter cell sized to the whole row.
|
|
||||||
pub(crate) fn gutter_cell<E>(
|
|
||||||
width: Pixels,
|
|
||||||
height: Pixels,
|
|
||||||
mono_font: &'static str,
|
|
||||||
text_size: Pixels,
|
|
||||||
color: Hsla,
|
|
||||||
content: E,
|
|
||||||
) -> gpui::Div
|
|
||||||
where
|
|
||||||
E: IntoElement,
|
|
||||||
{
|
|
||||||
div()
|
|
||||||
.w(width)
|
|
||||||
.h(height)
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.flex_none()
|
|
||||||
.font_family(mono_font)
|
|
||||||
.text_size(text_size)
|
|
||||||
.line_height(gpui::relative(1.0))
|
|
||||||
.text_color(color)
|
|
||||||
.child(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maps a highlight role to the theme's semantic palette.
|
|
||||||
pub(crate) fn role_color(role: Role, colors: ColorTokens, ramps: Ramps) -> Hsla {
|
|
||||||
match role {
|
|
||||||
Role::Text => colors.foreground,
|
|
||||||
Role::Keyword => ramps.primary.base,
|
|
||||||
Role::String | Role::Escape => ramps.success.base,
|
|
||||||
Role::Comment | Role::Punctuation => colors.muted_foreground,
|
|
||||||
Role::Function | Role::Property => ramps.info.base,
|
|
||||||
Role::Type => ramps.warning.base,
|
|
||||||
Role::Number | Role::Constant => ramps.secondary.base,
|
|
||||||
Role::Operator => colors.foreground,
|
|
||||||
Role::Tag => ramps.destructive.base,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+357
-133
@@ -1,21 +1,25 @@
|
|||||||
//! The diff view: a unified, syntax-highlighted rendering of two versions of
|
//! The diff view: a unified, syntax-highlighted rendering of two versions of
|
||||||
//! a file. Rows reuse the code view's highlighting; consumers that already
|
//! a file, in the style of the Pierre diff engine used by tangled.org: change
|
||||||
//! hold a git diff can build [`DiffRow`]s themselves instead of calling
|
//! bars instead of markers, tinted number gutters, word-level intra-line
|
||||||
//! [`diff_rows`].
|
//! highlights and hunk separators. 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::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Context, ElementId, HighlightStyle, Hsla, InteractiveElement as _, IntoElement,
|
App, Context, ElementId, HighlightStyle, InteractiveElement as _, IntoElement,
|
||||||
ListHorizontalSizingBehavior, ParentElement as _, Pixels, Render, ScrollStrategy, SharedString,
|
ListHorizontalSizingBehavior, ParentElement as _, Pixels, Render, ScrollStrategy, SharedString,
|
||||||
StatefulInteractiveElement as _, Styled as _, TextStyle, UniformListScrollHandle, Window, div,
|
StatefulInteractiveElement as _, Styled as _, TextStyle, UniformListScrollHandle, Window, div,
|
||||||
point, uniform_list,
|
point, uniform_list,
|
||||||
};
|
};
|
||||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
use gpui_native_theme::ActiveTheme as _;
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
|
|
||||||
use crate::code_view::{gutter_cell, role_color};
|
use std::ops::Range;
|
||||||
|
|
||||||
use crate::highlight::{Language, Line, Role, highlight};
|
use crate::highlight::{Language, Line, Role, highlight};
|
||||||
|
use crate::utils::{gutter_cell, line_row, role_color, wash, wash_gutter, wash_strong};
|
||||||
|
|
||||||
/// What a diff row did to the file.
|
/// What a diff row did to the file.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -26,18 +30,69 @@ pub enum DiffRowKind {
|
|||||||
Added,
|
Added,
|
||||||
/// Only in the old version.
|
/// Only in the old version.
|
||||||
Removed,
|
Removed,
|
||||||
|
/// A hunk separator introducing the rows after it.
|
||||||
|
Hunk,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row of a unified diff.
|
/// One row of a unified diff: a diff line or a hunk separator.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct DiffRow {
|
pub struct DiffRow {
|
||||||
pub kind: DiffRowKind,
|
pub kind: DiffRowKind,
|
||||||
/// The highlighted line content.
|
/// The highlighted line content; empty for hunk separators.
|
||||||
pub content: Line,
|
pub content: Line,
|
||||||
/// 1-based line number in the old version, when the row exists there.
|
/// 1-based line number in the old version, when the row exists there.
|
||||||
pub old_number: Option<usize>,
|
pub old_number: Option<usize>,
|
||||||
/// 1-based line number in the new version, when the row exists there.
|
/// 1-based line number in the new version, when the row exists there.
|
||||||
pub new_number: Option<usize>,
|
pub new_number: Option<usize>,
|
||||||
|
/// Byte ranges of intra-line word changes, tinted on changed rows.
|
||||||
|
pub changed_ranges: Vec<Range<usize>>,
|
||||||
|
/// The `@@ -a,b +c,d @@` header, for hunk separator rows.
|
||||||
|
pub header: SharedString,
|
||||||
|
/// Unchanged lines collapsed into this separator row; zero elsewhere.
|
||||||
|
pub hidden_lines: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How changed rows are marked.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum DiffIndicators {
|
||||||
|
/// A two-pixel bar on the row's left edge.
|
||||||
|
#[default]
|
||||||
|
Bars,
|
||||||
|
/// Classic `+`/`-` glyphs in a marker column.
|
||||||
|
Classic,
|
||||||
|
/// Only the background washes.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How collapsed regions between hunks render.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum HunkSeparator {
|
||||||
|
/// The collapsed unchanged line count, centered.
|
||||||
|
#[default]
|
||||||
|
LineInfo,
|
||||||
|
/// The patch-style `@@ -a,b +c,d @@` header.
|
||||||
|
Metadata,
|
||||||
|
/// A minimal `···` bar.
|
||||||
|
Simple,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Presentation options for the diff view.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct DiffViewOptions {
|
||||||
|
pub indicators: DiffIndicators,
|
||||||
|
pub separator: HunkSeparator,
|
||||||
|
/// Whether the line-number gutter renders.
|
||||||
|
pub line_numbers: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DiffViewOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
indicators: DiffIndicators::Bars,
|
||||||
|
separator: HunkSeparator::LineInfo,
|
||||||
|
line_numbers: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiffRow {
|
impl DiffRow {
|
||||||
@@ -58,23 +113,121 @@ pub fn diff_rows(old: &str, new: &str, language: Language) -> Vec<DiffRow> {
|
|||||||
let diff = TextDiff::from_lines(old, new);
|
let diff = TextDiff::from_lines(old, new);
|
||||||
|
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
|
let mut collapsed_before = 0usize;
|
||||||
|
|
||||||
for change in diff.iter_all_changes() {
|
for group in diff.grouped_ops(3) {
|
||||||
let code = match change.tag() {
|
let first = group.first().expect("hunk groups are never empty");
|
||||||
ChangeTag::Equal => change.new_index().and_then(|ix| new_code.lines.get(ix)),
|
let last = group.last().expect("hunk groups are never empty");
|
||||||
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)),
|
let old_range = first.old_range().start..last.old_range().end;
|
||||||
|
let new_range = first.new_range().start..last.new_range().end;
|
||||||
|
|
||||||
|
// git prints the line before a pure insertion when the count is zero.
|
||||||
|
let old_start = if old_range.is_empty() {
|
||||||
|
old_range.start
|
||||||
|
} else {
|
||||||
|
old_range.start + 1
|
||||||
};
|
};
|
||||||
rows.push(DiffRow {
|
|
||||||
kind: match change.tag() {
|
let new_start = if new_range.is_empty() {
|
||||||
ChangeTag::Equal => DiffRowKind::Context,
|
new_range.start
|
||||||
ChangeTag::Delete => DiffRowKind::Removed,
|
} else {
|
||||||
ChangeTag::Insert => DiffRowKind::Added,
|
new_range.start + 1
|
||||||
},
|
};
|
||||||
content: code.cloned().unwrap_or_default(),
|
|
||||||
old_number: change.old_index().map(|ix| ix + 1),
|
let collapsed = first.old_range().start - collapsed_before;
|
||||||
new_number: change.new_index().map(|ix| ix + 1),
|
if collapsed > 0 {
|
||||||
});
|
rows.push(DiffRow {
|
||||||
|
kind: DiffRowKind::Hunk,
|
||||||
|
content: Line::default(),
|
||||||
|
old_number: None,
|
||||||
|
new_number: None,
|
||||||
|
changed_ranges: Vec::new(),
|
||||||
|
header: format!(
|
||||||
|
"@@ -{},{} +{},{} @@",
|
||||||
|
old_start,
|
||||||
|
old_range.len(),
|
||||||
|
new_start,
|
||||||
|
new_range.len()
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
hidden_lines: collapsed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
collapsed_before = last.old_range().end;
|
||||||
|
|
||||||
|
for op in &group {
|
||||||
|
let changes: Vec<_> = diff.iter_changes(op).collect();
|
||||||
|
let mut ix = 0;
|
||||||
|
|
||||||
|
while ix < changes.len() {
|
||||||
|
if changes[ix].tag() == ChangeTag::Equal {
|
||||||
|
rows.push(line_row(&changes[ix], &new_code, Vec::new()));
|
||||||
|
ix += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deletions = changes[ix..]
|
||||||
|
.iter()
|
||||||
|
.take_while(|change| change.tag() == ChangeTag::Delete)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let insertions = changes[ix + deletions..]
|
||||||
|
.iter()
|
||||||
|
.take_while(|change| change.tag() == ChangeTag::Insert)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let mut deletion_ranges = vec![Vec::new(); deletions];
|
||||||
|
let mut insertion_ranges = vec![Vec::new(); insertions];
|
||||||
|
|
||||||
|
for pair in 0..deletions.min(insertions) {
|
||||||
|
let old_text = changes[ix + pair].value().trim_end_matches('\n');
|
||||||
|
let new_text = changes[ix + deletions + pair]
|
||||||
|
.value()
|
||||||
|
.trim_end_matches('\n');
|
||||||
|
let words = TextDiff::from_words(old_text, new_text);
|
||||||
|
|
||||||
|
let (mut old_offset, mut new_offset) = (0usize, 0usize);
|
||||||
|
|
||||||
|
for word in words.iter_all_changes() {
|
||||||
|
let len = word.value().len();
|
||||||
|
|
||||||
|
match word.tag() {
|
||||||
|
ChangeTag::Delete => {
|
||||||
|
deletion_ranges[pair].push(old_offset..old_offset + len);
|
||||||
|
old_offset += len;
|
||||||
|
}
|
||||||
|
ChangeTag::Insert => {
|
||||||
|
insertion_ranges[pair].push(new_offset..new_offset + len);
|
||||||
|
new_offset += len;
|
||||||
|
}
|
||||||
|
ChangeTag::Equal => {
|
||||||
|
old_offset += len;
|
||||||
|
new_offset += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for pair in 0..deletions {
|
||||||
|
rows.push(line_row(
|
||||||
|
&changes[ix + pair],
|
||||||
|
&old_code,
|
||||||
|
deletion_ranges[pair].clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for pair in 0..insertions {
|
||||||
|
rows.push(line_row(
|
||||||
|
&changes[ix + deletions + pair],
|
||||||
|
&new_code,
|
||||||
|
insertion_ranges[pair].clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ix += deletions + insertions;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rows
|
rows
|
||||||
@@ -84,6 +237,7 @@ pub fn diff_rows(old: &str, new: &str, language: Language) -> Vec<DiffRow> {
|
|||||||
pub struct DiffViewState {
|
pub struct DiffViewState {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
rows: Vec<DiffRow>,
|
rows: Vec<DiffRow>,
|
||||||
|
options: DiffViewOptions,
|
||||||
/// Drives both scroll axes of the virtualized row list.
|
/// Drives both scroll axes of the virtualized row list.
|
||||||
handle: UniformListScrollHandle,
|
handle: UniformListScrollHandle,
|
||||||
a11y_label: Option<SharedString>,
|
a11y_label: Option<SharedString>,
|
||||||
@@ -100,11 +254,30 @@ impl DiffViewState {
|
|||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
rows: diff_rows(old.as_ref(), new.as_ref(), language),
|
rows: diff_rows(old.as_ref(), new.as_ref(), language),
|
||||||
|
options: DiffViewOptions::default(),
|
||||||
handle: UniformListScrollHandle::new(),
|
handle: UniformListScrollHandle::new(),
|
||||||
a11y_label: None,
|
a11y_label: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets how changed rows are marked.
|
||||||
|
pub fn indicators(mut self, indicators: DiffIndicators) -> Self {
|
||||||
|
self.options.indicators = indicators;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets how collapsed regions between hunks render.
|
||||||
|
pub fn hunk_separator(mut self, separator: HunkSeparator) -> Self {
|
||||||
|
self.options.separator = separator;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows or hides the line-number gutter.
|
||||||
|
pub fn line_numbers(mut self, show: bool) -> Self {
|
||||||
|
self.options.line_numbers = show;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Replaces the versions and re-diffs them.
|
/// 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>)
|
pub fn set_versions<O, N>(&mut self, old: O, new: N, language: Language, cx: &mut Context<Self>)
|
||||||
where
|
where
|
||||||
@@ -124,12 +297,12 @@ impl DiffViewState {
|
|||||||
&self.rows
|
&self.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls the first non-context row to the top of the view.
|
/// Scrolls the first added or removed row to the top of the view.
|
||||||
pub fn scroll_to_first_change(&self) {
|
pub fn scroll_to_first_change(&self) {
|
||||||
let Some(row) = self
|
let Some(row) = self
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.position(|row| row.kind != DiffRowKind::Context)
|
.position(|row| matches!(row.kind, DiffRowKind::Added | DiffRowKind::Removed))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -150,53 +323,25 @@ impl Render for DiffViewState {
|
|||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let text_size = theme.sizes.small.text_size;
|
let text_size = theme.sizes.small.text_size;
|
||||||
let line_height = text_size * 1.5;
|
|
||||||
|
|
||||||
let id = self.id.clone();
|
let id = self.id.clone();
|
||||||
let state = cx.entity();
|
let state = cx.entity();
|
||||||
let row_count = self.rows.len();
|
let row_count = self.rows.len();
|
||||||
|
let options = self.options;
|
||||||
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
|
let widest_row = self
|
||||||
.rows
|
.rows
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
.filter(|(_, row)| row.kind != DiffRowKind::Hunk)
|
||||||
.max_by_key(|(_, row)| row.char_count())
|
.max_by_key(|(_, row)| row.char_count())
|
||||||
.map(|(ix, _)| ix);
|
.map(|(ix, _)| ix);
|
||||||
|
|
||||||
let mut rows = uniform_list((id.clone(), "rows"), row_count, {
|
let mut rows = uniform_list((id.clone(), "rows"), row_count, {
|
||||||
let id = id.clone();
|
|
||||||
move |range, _window, cx| {
|
move |range, _window, cx| {
|
||||||
let diff = state.read(cx);
|
let diff = state.read(cx);
|
||||||
let theme = cx.theme();
|
|
||||||
range
|
range
|
||||||
.map(|row_ix| {
|
.map(|row_ix| render_row(&diff.rows[row_ix], row_ix, options, cx))
|
||||||
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()
|
.collect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -230,55 +375,134 @@ impl Render for DiffViewState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn render_row(
|
fn render_row(
|
||||||
row: &DiffRow,
|
row: &DiffRow,
|
||||||
id: &ElementId,
|
|
||||||
row_ix: usize,
|
row_ix: usize,
|
||||||
mono_font: &'static str,
|
options: DiffViewOptions,
|
||||||
colors: ColorTokens,
|
cx: &App,
|
||||||
ramps: Ramps,
|
|
||||||
text_size: Pixels,
|
|
||||||
line_height: Pixels,
|
|
||||||
char_width: Pixels,
|
|
||||||
marker_width: Pixels,
|
|
||||||
number_width: Pixels,
|
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let mono_font = theme.mono_font;
|
||||||
|
let colors = theme.colors;
|
||||||
|
let ramps = theme.ramps;
|
||||||
|
let text_size = theme.sizes.small.text_size;
|
||||||
|
let line_height = text_size * 1.5;
|
||||||
|
let char_width = text_size * 0.62;
|
||||||
|
|
||||||
|
let frame = div()
|
||||||
|
.debug_selector(move || format!("diff-row-{row_ix}"))
|
||||||
|
.id(("diff-row", row_ix))
|
||||||
|
.w_full()
|
||||||
|
.h(line_height)
|
||||||
|
.font_family(mono_font)
|
||||||
|
.text_size(text_size)
|
||||||
|
.overflow_hidden();
|
||||||
|
|
||||||
|
if row.kind == DiffRowKind::Hunk {
|
||||||
|
let content = match options.separator {
|
||||||
|
HunkSeparator::LineInfo => {
|
||||||
|
SharedString::from(format!("{} unchanged lines", row.hidden_lines))
|
||||||
|
}
|
||||||
|
HunkSeparator::Metadata => row.header.clone(),
|
||||||
|
HunkSeparator::Simple => SharedString::from("···"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return frame
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.text_color(colors.muted_foreground)
|
||||||
|
.when(
|
||||||
|
options.indicators == DiffIndicators::Classic,
|
||||||
|
|row_element| row_element.child(div().w_10().flex_none()),
|
||||||
|
)
|
||||||
|
.when(options.line_numbers, |row_element| {
|
||||||
|
row_element.child(div().w_10().flex_none())
|
||||||
|
})
|
||||||
|
.when(
|
||||||
|
options.indicators == DiffIndicators::Bars && !options.line_numbers,
|
||||||
|
|row_element| {
|
||||||
|
row_element.child(
|
||||||
|
div()
|
||||||
|
.flex_none()
|
||||||
|
.border_l_4()
|
||||||
|
.border_color(gpui::transparent_black()),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.child(div().flex_none().ml_2().child(content))
|
||||||
|
.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let change_color = match row.kind {
|
||||||
|
DiffRowKind::Added => ramps.success.base,
|
||||||
|
DiffRowKind::Removed => ramps.destructive.base,
|
||||||
|
DiffRowKind::Context | DiffRowKind::Hunk => gpui::transparent_black(),
|
||||||
|
};
|
||||||
|
|
||||||
let wash = match row.kind {
|
let wash = match row.kind {
|
||||||
DiffRowKind::Context => None,
|
|
||||||
DiffRowKind::Added => Some(wash(ramps.success.base)),
|
DiffRowKind::Added => Some(wash(ramps.success.base)),
|
||||||
DiffRowKind::Removed => Some(wash(ramps.destructive.base)),
|
DiffRowKind::Removed => Some(wash(ramps.destructive.base)),
|
||||||
};
|
DiffRowKind::Context | DiffRowKind::Hunk => None,
|
||||||
|
|
||||||
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 text = String::new();
|
||||||
let mut highlights: Vec<(std::ops::Range<usize>, HighlightStyle)> = Vec::new();
|
let mut spans: Vec<(Range<usize>, Role)> = Vec::new();
|
||||||
|
|
||||||
for span in &row.content.spans {
|
for span in &row.content.spans {
|
||||||
let start = text.len();
|
let start = text.len();
|
||||||
text.push_str(&span.text);
|
text.push_str(&span.text);
|
||||||
if span.role != Role::Text {
|
if span.role != Role::Text {
|
||||||
highlights.push((
|
spans.push((start..text.len(), span.role));
|
||||||
start..text.len(),
|
|
||||||
HighlightStyle {
|
|
||||||
color: Some(role_color(span.role, colors, ramps)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut boundaries: Vec<usize> = spans
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(range, _)| [range.start, range.end])
|
||||||
|
.chain(
|
||||||
|
row.changed_ranges
|
||||||
|
.iter()
|
||||||
|
.flat_map(|range| [range.start, range.end]),
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
boundaries.sort_unstable();
|
||||||
|
boundaries.dedup();
|
||||||
|
|
||||||
|
let mut highlights: Vec<(std::ops::Range<usize>, HighlightStyle)> = Vec::new();
|
||||||
|
let mut span_ix = 0;
|
||||||
|
|
||||||
|
for window in boundaries.windows(2) {
|
||||||
|
let (start, end) = (window[0], window[1]);
|
||||||
|
while span_ix < spans.len() && spans[span_ix].0.end <= start {
|
||||||
|
span_ix += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut segment = HighlightStyle::default();
|
||||||
|
if let Some((range, role)) = spans.get(span_ix)
|
||||||
|
&& range.contains(&start)
|
||||||
|
&& *role != Role::Text
|
||||||
|
{
|
||||||
|
segment = segment.highlight(HighlightStyle {
|
||||||
|
color: Some(role_color(*role, colors, ramps)),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if row
|
||||||
|
.changed_ranges
|
||||||
|
.iter()
|
||||||
|
.any(|range| range.contains(&start))
|
||||||
|
{
|
||||||
|
segment = segment.highlight(HighlightStyle {
|
||||||
|
background_color: Some(wash_strong(change_color)),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
highlights.push((start..end, segment));
|
||||||
|
}
|
||||||
|
|
||||||
let style = TextStyle {
|
let style = TextStyle {
|
||||||
color: colors.foreground,
|
color: colors.foreground,
|
||||||
font_family: SharedString::from(mono_font),
|
font_family: SharedString::from(mono_font),
|
||||||
@@ -287,67 +511,67 @@ fn render_row(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
frame
|
||||||
.debug_selector({
|
|
||||||
let name = format!("{id}-row-{row_ix}");
|
|
||||||
move || name
|
|
||||||
})
|
|
||||||
.id(("diff-row", row_ix))
|
|
||||||
.h(line_height)
|
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.overflow_hidden()
|
.when_some(wash, |row_element, tint| row_element.bg(tint))
|
||||||
.when_some(wash, |row_element, wash| row_element.bg(wash))
|
.when(
|
||||||
.child(gutter_cell(
|
options.indicators == DiffIndicators::Classic,
|
||||||
marker_width,
|
|row_element| {
|
||||||
line_height,
|
row_element.child(gutter_cell(
|
||||||
mono_font,
|
cx,
|
||||||
text_size,
|
if row.kind == DiffRowKind::Context {
|
||||||
marker_color.unwrap_or(gpui::transparent_black()),
|
gpui::transparent_black()
|
||||||
marker,
|
} else {
|
||||||
))
|
change_color
|
||||||
.child(
|
},
|
||||||
gutter_cell(
|
match row.kind {
|
||||||
number_width,
|
DiffRowKind::Added => "+",
|
||||||
line_height,
|
DiffRowKind::Removed => "-",
|
||||||
mono_font,
|
_ => "",
|
||||||
text_size,
|
},
|
||||||
colors.muted_foreground,
|
))
|
||||||
row.old_number
|
},
|
||||||
.map(|number| number.to_string())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.justify_end()
|
|
||||||
.whitespace_nowrap(),
|
|
||||||
)
|
)
|
||||||
.child(
|
.when(options.line_numbers, |row_element| {
|
||||||
gutter_cell(
|
let mut number = gutter_cell(
|
||||||
number_width,
|
cx,
|
||||||
line_height,
|
|
||||||
mono_font,
|
|
||||||
text_size,
|
|
||||||
colors.muted_foreground,
|
colors.muted_foreground,
|
||||||
row.new_number
|
row.new_number
|
||||||
|
.or(row.old_number)
|
||||||
.map(|number| number.to_string())
|
.map(|number| number.to_string())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
.justify_end()
|
.w_10()
|
||||||
.whitespace_nowrap(),
|
.whitespace_nowrap()
|
||||||
|
.bg(match row.kind {
|
||||||
|
DiffRowKind::Context => wash_gutter(&colors),
|
||||||
|
_ => wash_strong(change_color),
|
||||||
|
});
|
||||||
|
if options.indicators == DiffIndicators::Bars {
|
||||||
|
number = number.border_l_4().border_color(change_color);
|
||||||
|
}
|
||||||
|
row_element.child(number)
|
||||||
|
})
|
||||||
|
.when(
|
||||||
|
options.indicators == DiffIndicators::Bars && !options.line_numbers,
|
||||||
|
|row_element| {
|
||||||
|
row_element.child(
|
||||||
|
div()
|
||||||
|
.h_full()
|
||||||
|
.flex_none()
|
||||||
|
.border_l_4()
|
||||||
|
.border_color(change_color),
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
|
.ml_2()
|
||||||
.w(char_width * row.char_count() as f32)
|
.w(char_width * row.char_count() as f32)
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.child(gpui::StyledText::new(text).with_default_highlights(&style, highlights)),
|
.child(gpui::StyledText::new(text).with_default_highlights(&style, highlights)),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ pub mod code_view;
|
|||||||
pub mod diff_view;
|
pub mod diff_view;
|
||||||
|
|
||||||
pub mod highlight;
|
pub mod highlight;
|
||||||
|
|
||||||
|
mod utils;
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
//! Shared helpers for the editor's views: gutter cells, role colors, change
|
||||||
|
//! washes and diff row building.
|
||||||
|
|
||||||
|
#[cfg(feature = "diff_view")]
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{App, HighlightStyle, Hsla, IntoElement, ParentElement as _, Styled as _, div};
|
||||||
|
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
||||||
|
#[cfg(feature = "diff_view")]
|
||||||
|
use similar::{Change, ChangeTag};
|
||||||
|
|
||||||
|
#[cfg(feature = "diff_view")]
|
||||||
|
use crate::diff_view::{DiffRow, DiffRowKind};
|
||||||
|
#[cfg(feature = "diff_view")]
|
||||||
|
use crate::highlight::HighlightedCode;
|
||||||
|
use crate::highlight::Role;
|
||||||
|
|
||||||
|
/// Maps a highlight role to the theme's semantic palette.
|
||||||
|
pub(crate) fn role_color(role: Role, colors: ColorTokens, ramps: Ramps) -> Hsla {
|
||||||
|
match role {
|
||||||
|
Role::Text => colors.foreground,
|
||||||
|
Role::Keyword => ramps.primary.base,
|
||||||
|
Role::String | Role::Escape => ramps.success.base,
|
||||||
|
Role::Comment | Role::Punctuation => colors.muted_foreground,
|
||||||
|
Role::Function | Role::Property => ramps.info.base,
|
||||||
|
Role::Type => ramps.warning.base,
|
||||||
|
Role::Number | Role::Constant => ramps.secondary.base,
|
||||||
|
Role::Operator => colors.foreground,
|
||||||
|
Role::Tag => ramps.destructive.base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the highlight style for one run of a code view row.
|
||||||
|
pub(crate) fn run_style(
|
||||||
|
role: Role,
|
||||||
|
selected: bool,
|
||||||
|
colors: ColorTokens,
|
||||||
|
ramps: Ramps,
|
||||||
|
) -> HighlightStyle {
|
||||||
|
HighlightStyle {
|
||||||
|
color: Some(role_color(role, colors, ramps)),
|
||||||
|
background_color: selected.then_some(colors.selection),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A gutter cell sized to the whole row.
|
||||||
|
pub(crate) fn gutter_cell<E>(cx: &App, color: Hsla, content: E) -> gpui::Div
|
||||||
|
where
|
||||||
|
E: IntoElement,
|
||||||
|
{
|
||||||
|
let theme = cx.theme();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.size_10()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.flex_none()
|
||||||
|
.font_family(theme.mono_font)
|
||||||
|
.text_size(theme.sizes.small.text_size)
|
||||||
|
.line_height(gpui::relative(1.0))
|
||||||
|
.text_color(color)
|
||||||
|
.child(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a diff line row from a `similar` change.
|
||||||
|
#[cfg(feature = "diff_view")]
|
||||||
|
pub(crate) fn line_row(
|
||||||
|
change: &Change<&str>,
|
||||||
|
code: &HighlightedCode,
|
||||||
|
changed_ranges: Vec<Range<usize>>,
|
||||||
|
) -> DiffRow {
|
||||||
|
let content = match change.tag() {
|
||||||
|
ChangeTag::Equal => change
|
||||||
|
.new_index()
|
||||||
|
.and_then(|ix| code.lines.get(ix))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
ChangeTag::Delete => change
|
||||||
|
.old_index()
|
||||||
|
.and_then(|ix| code.lines.get(ix))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
ChangeTag::Insert => change
|
||||||
|
.new_index()
|
||||||
|
.and_then(|ix| code.lines.get(ix))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (old_number, new_number) = match change.tag() {
|
||||||
|
ChangeTag::Equal => (
|
||||||
|
change.old_index().map(|ix| ix + 1),
|
||||||
|
change.new_index().map(|ix| ix + 1),
|
||||||
|
),
|
||||||
|
ChangeTag::Delete => (change.old_index().map(|ix| ix + 1), None),
|
||||||
|
ChangeTag::Insert => (None, change.new_index().map(|ix| ix + 1)),
|
||||||
|
};
|
||||||
|
|
||||||
|
DiffRow {
|
||||||
|
kind: match change.tag() {
|
||||||
|
ChangeTag::Equal => DiffRowKind::Context,
|
||||||
|
ChangeTag::Delete => DiffRowKind::Removed,
|
||||||
|
ChangeTag::Insert => DiffRowKind::Added,
|
||||||
|
},
|
||||||
|
content,
|
||||||
|
old_number,
|
||||||
|
new_number,
|
||||||
|
changed_ranges,
|
||||||
|
header: Default::default(),
|
||||||
|
hidden_lines: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A change wash: the ramp color at the alpha rows tint with.
|
||||||
|
pub(crate) fn wash(color: Hsla) -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
a: color.a * 0.14,
|
||||||
|
..color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stronger wash under number gutters and intra-line word changes.
|
||||||
|
pub(crate) fn wash_strong(color: Hsla) -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
a: color.a * 0.34,
|
||||||
|
..color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The subtle wash that sets the number gutter off from the code.
|
||||||
|
pub(crate) fn wash_gutter(colors: &ColorTokens) -> Hsla {
|
||||||
|
Hsla {
|
||||||
|
a: colors.muted.a * 0.18,
|
||||||
|
..colors.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ use gpui::{
|
|||||||
AppContext as _, Context, Entity, IntoElement, MouseButton, ParentElement as _, Render,
|
AppContext as _, Context, Entity, IntoElement, MouseButton, ParentElement as _, Render,
|
||||||
Styled as _, TestAppContext, VisualTestContext, Window, div, point, px,
|
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};
|
use gpui_native_editor::highlight::{Language, Role};
|
||||||
|
|
||||||
const OLD: &str = "fn a() {}\nfn b() {}\nfn c() {}\n";
|
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,
|
DiffRowKind::Context,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
rows.iter().all(|row| row.kind != DiffRowKind::Hunk),
|
||||||
|
"changes within one context window need no separator"
|
||||||
|
);
|
||||||
|
|
||||||
let removed = &rows[1];
|
let removed = &rows[1];
|
||||||
assert_eq!(removed.old_number, Some(2));
|
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));
|
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]
|
#[test]
|
||||||
fn an_added_line_carries_its_highlight_spans() {
|
fn an_added_line_carries_its_highlight_spans() {
|
||||||
let rows = diff_rows("", "fn zed() {}\n", Language::Rust);
|
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 {
|
struct Host {
|
||||||
diff: Entity<DiffViewState>,
|
diff: Entity<DiffViewState>,
|
||||||
}
|
}
|
||||||
@@ -90,18 +145,17 @@ impl Render for Host {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn diff_view_window(
|
fn diff_view_window(
|
||||||
rows: usize,
|
lines: usize,
|
||||||
changes_at: usize,
|
changed_lines: &[usize],
|
||||||
) -> (TestAppContext, VisualTestContext, Entity<DiffViewState>) {
|
) -> (TestAppContext, VisualTestContext, Entity<DiffViewState>) {
|
||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
cx.update(gpui_native_theme::Theme::init);
|
cx.update(gpui_native_theme::Theme::init);
|
||||||
let mut old = String::new();
|
let mut old = String::new();
|
||||||
let mut new = 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"));
|
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} rewritten\n"));
|
||||||
new.push_str(&format!("line {line} added\n"));
|
|
||||||
} else {
|
} else {
|
||||||
new.push_str(&format!("line {line} unchanged\n"));
|
new.push_str(&format!("line {line} unchanged\n"));
|
||||||
}
|
}
|
||||||
@@ -124,17 +178,33 @@ fn diff_view_window(
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rows_render_virtualized_and_the_view_jumps_to_the_first_change() {
|
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!(
|
assert!(
|
||||||
visual.debug_bounds("test-diff-row-0").is_some(),
|
visual.debug_bounds("diff-row-0").is_some(),
|
||||||
"the first row renders"
|
"the first row renders"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
visual.debug_bounds("test-diff-row-199").is_none(),
|
visual.debug_bounds("diff-row-100").is_none(),
|
||||||
"rows past the viewport never render"
|
"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.update_in(&mut visual, |state, _window, _cx| {
|
||||||
state.scroll_to_first_change()
|
state.scroll_to_first_change()
|
||||||
});
|
});
|
||||||
@@ -143,13 +213,23 @@ fn rows_render_virtualized_and_the_view_jumps_to_the_first_change() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
visual.debug_bounds("test-diff-row-150").is_some(),
|
visual.debug_bounds("diff-row-4").is_some(),
|
||||||
"the first change scrolled into view"
|
"the first change scrolled into view"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
visual.debug_bounds("test-diff-row-0").is_none(),
|
visual.debug_bounds("diff-row-0").is_none(),
|
||||||
"the context before it scrolled out"
|
"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]
|
#[test]
|
||||||
@@ -163,14 +243,8 @@ fn set_versions_re_diffs() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let all_context = cx.update(|cx| {
|
let empty = cx.update(|cx| state.read(cx).rows().is_empty());
|
||||||
state
|
assert!(empty, "identical versions have no diff to show");
|
||||||
.read(cx)
|
|
||||||
.rows()
|
|
||||||
.iter()
|
|
||||||
.all(|row| row.kind == DiffRowKind::Context)
|
|
||||||
});
|
|
||||||
assert!(all_context, "identical versions diff to context rows");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -193,7 +267,7 @@ fn scrolling_horizontally_moves_rows_without_vertical_drift() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let before = visual
|
let before = visual
|
||||||
.debug_bounds("test-diff-row-0")
|
.debug_bounds("diff-row-0")
|
||||||
.expect("the first row renders");
|
.expect("the first row renders");
|
||||||
|
|
||||||
visual.simulate_event(gpui::ScrollEvent {
|
visual.simulate_event(gpui::ScrollEvent {
|
||||||
@@ -206,7 +280,7 @@ fn scrolling_horizontally_moves_rows_without_vertical_drift() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let after = visual
|
let after = visual
|
||||||
.debug_bounds("test-diff-row-0")
|
.debug_bounds("diff-row-0")
|
||||||
.expect("the first row still renders");
|
.expect("the first row still renders");
|
||||||
assert!(
|
assert!(
|
||||||
(after.origin.x - before.origin.x + px(200.)).abs() <= px(2.),
|
(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() {
|
fn scrolling_vertically_does_not_drift_horizontally() {
|
||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
cx.update(gpui_native_theme::Theme::init);
|
cx.update(gpui_native_theme::Theme::init);
|
||||||
let old = "fn same() {}\n".repeat(200);
|
// Spread changes keep the diff taller than the viewport, so there is
|
||||||
let new = format!("{old}fn long() {{ {} }}\n", "text ".repeat(300));
|
// 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 = DiffViewState::new("test-diff", old, new, Language::Rust);
|
||||||
let state_cell: Rc<RefCell<Option<Entity<DiffViewState>>>> = Rc::new(RefCell::new(None));
|
let state_cell: Rc<RefCell<Option<Entity<DiffViewState>>>> = Rc::new(RefCell::new(None));
|
||||||
let cell = state_cell.clone();
|
let cell = state_cell.clone();
|
||||||
@@ -238,7 +323,7 @@ fn scrolling_vertically_does_not_drift_horizontally() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let before = visual
|
let before = visual
|
||||||
.debug_bounds("test-diff-row-0")
|
.debug_bounds("diff-row-0")
|
||||||
.expect("the first row renders");
|
.expect("the first row renders");
|
||||||
|
|
||||||
// A pure vertical gesture...
|
// A pure vertical gesture...
|
||||||
@@ -259,7 +344,7 @@ fn scrolling_vertically_does_not_drift_horizontally() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let after = visual
|
let after = visual
|
||||||
.debug_bounds("test-diff-row-0")
|
.debug_bounds("diff-row-0")
|
||||||
.expect("the first row still renders");
|
.expect("the first row still renders");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
after.origin.x, before.origin.x,
|
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() {
|
fn dragging_the_vertical_thumb_leaves_the_horizontal_offset_alone() {
|
||||||
let mut cx = TestAppContext::single();
|
let mut cx = TestAppContext::single();
|
||||||
cx.update(gpui_native_theme::Theme::init);
|
cx.update(gpui_native_theme::Theme::init);
|
||||||
let old = "fn same() {}\n".repeat(200);
|
// Spread changes keep the diff taller than the viewport, so the vertical
|
||||||
let new = format!("{old}fn long() {{ {} }}\n", "text ".repeat(300));
|
// 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 = DiffViewState::new("test-diff", old, new, Language::Rust);
|
||||||
let state_cell: Rc<RefCell<Option<Entity<DiffViewState>>>> = Rc::new(RefCell::new(None));
|
let state_cell: Rc<RefCell<Option<Entity<DiffViewState>>>> = Rc::new(RefCell::new(None));
|
||||||
let cell = state_cell.clone();
|
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.
|
// Scroll the strip left first, so horizontal drift would be observable.
|
||||||
let row = visual
|
let row = visual
|
||||||
.debug_bounds("test-diff-row-0")
|
.debug_bounds("diff-row-0")
|
||||||
.expect("the first row renders");
|
.expect("the first row renders");
|
||||||
visual.simulate_event(gpui::ScrollEvent {
|
visual.simulate_event(gpui::ScrollEvent {
|
||||||
position: row.center(),
|
position: row.center(),
|
||||||
@@ -303,7 +399,7 @@ fn dragging_the_vertical_thumb_leaves_the_horizontal_offset_alone() {
|
|||||||
window.draw(cx).clear(cx);
|
window.draw(cx).clear(cx);
|
||||||
});
|
});
|
||||||
let before = visual
|
let before = visual
|
||||||
.debug_bounds("test-diff-row-0")
|
.debug_bounds("diff-row-0")
|
||||||
.expect("the first row still renders");
|
.expect("the first row still renders");
|
||||||
|
|
||||||
// Press the vertical thumb: right edge, within its minimum length.
|
// 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
|
let after = visual
|
||||||
.debug_bounds("test-diff-row-0")
|
.debug_bounds("diff-row-0")
|
||||||
.expect("the first row still renders");
|
.expect("the first row still renders");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
after.origin.x, before.origin.x,
|
after.origin.x, before.origin.x,
|
||||||
|
|||||||
Reference in New Issue
Block a user