chore: audit components
This commit is contained in:
+243
-144
@@ -3,7 +3,7 @@
|
||||
//! bars instead of markers, tinted number gutters, word-level intra-line
|
||||
//! 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`].
|
||||
//! instead of calling [`DiffViewState::diff_rows`].
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
@@ -14,12 +14,12 @@ use gpui::{
|
||||
};
|
||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||
use gpui_native_theme::{ActiveTheme as _, MONO_ADVANCE_RATIO};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use similar::{Change, ChangeTag, TextDiff};
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::highlight::{Language, Line, Role, highlight};
|
||||
use crate::utils::{gutter_cell, line_row, role_color, wash, wash_gutter, wash_strong};
|
||||
use crate::highlight::{HighlightedCode, Language, Line, Role, highlight};
|
||||
use crate::utils::{gutter_cell, wash, wash_gutter, wash_strong};
|
||||
|
||||
/// What a diff row did to the file.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -96,147 +96,82 @@ impl Default for DiffViewOptions {
|
||||
}
|
||||
|
||||
impl DiffRow {
|
||||
/// Assembles a diff line row from a `similar` change.
|
||||
pub(crate) fn from_change(
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// The row's text length, in characters.
|
||||
fn char_count(&self) -> usize {
|
||||
self.content
|
||||
.spans
|
||||
.iter()
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum()
|
||||
self.content.text().chars().count()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
let mut collapsed_before = 0usize;
|
||||
|
||||
for group in diff.grouped_ops(3) {
|
||||
let first = group.first().expect("hunk groups are never empty");
|
||||
let last = group.last().expect("hunk groups are never empty");
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
let new_start = if new_range.is_empty() {
|
||||
new_range.start
|
||||
} else {
|
||||
new_range.start + 1
|
||||
};
|
||||
|
||||
let collapsed = first.old_range().start - collapsed_before;
|
||||
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;
|
||||
/// Stable element id: the kind and line numbers packed into one integer.
|
||||
fn element_id(&self, hunk_ordinal: usize) -> ElementId {
|
||||
let kind = match self.kind {
|
||||
DiffRowKind::Context => 0u64,
|
||||
DiffRowKind::Added => 1,
|
||||
DiffRowKind::Removed => 2,
|
||||
DiffRowKind::Hunk => {
|
||||
return ElementId::named_usize("diff-hunk", hunk_ordinal);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Line numbers saturate at 2^31 so the two fields never overlap.
|
||||
let old = self.old_number.unwrap_or(0).min((1 << 31) - 1) as u64;
|
||||
let new = self.new_number.unwrap_or(0).min((1 << 31) - 1) as u64;
|
||||
ElementId::named_usize("diff-row", ((kind << 62) | (old << 31) | new) as usize)
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
/// Retained state for a diff view.
|
||||
pub struct DiffViewState {
|
||||
id: ElementId,
|
||||
rows: Vec<DiffRow>,
|
||||
/// Cached per-row id and text length, parallel to `rows`.
|
||||
row_layouts: Vec<RowLayout>,
|
||||
/// The row whose width drives horizontal scroll extent, `rows`-derived.
|
||||
widest_row: Option<usize>,
|
||||
options: DiffViewOptions,
|
||||
@@ -245,6 +180,12 @@ pub struct DiffViewState {
|
||||
a11y_label: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// Cached per-row render data, computed once when the rows are set.
|
||||
struct RowLayout {
|
||||
id: ElementId,
|
||||
char_count: usize,
|
||||
}
|
||||
|
||||
impl DiffViewState {
|
||||
/// Builds the state, diffing `old` against `new`.
|
||||
pub fn new<I, O, N>(id: I, old: O, new: N, language: Language) -> Self
|
||||
@@ -256,6 +197,7 @@ impl DiffViewState {
|
||||
let mut state = Self {
|
||||
id: id.into(),
|
||||
rows: Vec::new(),
|
||||
row_layouts: Vec::new(),
|
||||
widest_row: None,
|
||||
options: DiffViewOptions::default(),
|
||||
handle: UniformListScrollHandle::new(),
|
||||
@@ -265,20 +207,168 @@ impl DiffViewState {
|
||||
state
|
||||
}
|
||||
|
||||
/// Replaces the rows, recomputing `widest_row` with them.
|
||||
/// Replaces the rows, recomputing the per-row layout cache with them.
|
||||
fn set_rows<O, N>(&mut self, old: O, new: N, language: Language)
|
||||
where
|
||||
O: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
self.rows = diff_rows(old.as_ref(), new.as_ref(), language);
|
||||
self.rows = Self::diff_rows(old.as_ref(), new.as_ref(), language);
|
||||
|
||||
let mut hunk_ordinal = 0usize;
|
||||
self.row_layouts = self
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let ordinal = if row.kind == DiffRowKind::Hunk {
|
||||
hunk_ordinal += 1;
|
||||
hunk_ordinal
|
||||
} else {
|
||||
0
|
||||
};
|
||||
RowLayout {
|
||||
id: row.element_id(ordinal),
|
||||
char_count: row.char_count(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.widest_row = self
|
||||
.rows
|
||||
.iter()
|
||||
.zip(&self.row_layouts)
|
||||
.enumerate()
|
||||
.filter(|(_, row)| row.kind != DiffRowKind::Hunk)
|
||||
.max_by_key(|(_, row)| row.char_count())
|
||||
.map(|(ix, _)| ix);
|
||||
.filter(|(_, (row, _))| row.kind != DiffRowKind::Hunk)
|
||||
.max_by_key(|(_, (_, layout))| layout.char_count)
|
||||
.map(|(widest, _)| widest);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
let mut collapsed_before = 0usize;
|
||||
|
||||
for group in diff.grouped_ops(3) {
|
||||
let (Some(first), Some(last)) = (group.first(), group.last()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
let new_start = if new_range.is_empty() {
|
||||
new_range.start
|
||||
} else {
|
||||
new_range.start + 1
|
||||
};
|
||||
|
||||
let collapsed = first.old_range().start.saturating_sub(collapsed_before);
|
||||
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 index = 0;
|
||||
|
||||
while index < changes.len() {
|
||||
if changes[index].tag() == ChangeTag::Equal {
|
||||
rows.push(DiffRow::from_change(&changes[index], &new_code, Vec::new()));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let deletions = changes[index..]
|
||||
.iter()
|
||||
.take_while(|change| change.tag() == ChangeTag::Delete)
|
||||
.count();
|
||||
|
||||
let insertions = changes[index + 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[index + pair].value().trim_end_matches('\n');
|
||||
let new_text = changes[index + 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(DiffRow::from_change(
|
||||
&changes[index + pair],
|
||||
&old_code,
|
||||
deletion_ranges[pair].clone(),
|
||||
));
|
||||
}
|
||||
|
||||
for pair in 0..insertions {
|
||||
rows.push(DiffRow::from_change(
|
||||
&changes[index + deletions + pair],
|
||||
&new_code,
|
||||
insertion_ranges[pair].clone(),
|
||||
));
|
||||
}
|
||||
|
||||
index += deletions + insertions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
/// Sets how changed rows are marked.
|
||||
@@ -355,7 +445,15 @@ impl Render for DiffViewState {
|
||||
move |range, _window, cx| {
|
||||
let diff = state.read(cx);
|
||||
range
|
||||
.map(|row_ix| render_row(&diff.rows[row_ix], row_ix, options, cx))
|
||||
.map(|row_index| {
|
||||
render_row(
|
||||
&diff.rows[row_index],
|
||||
&diff.row_layouts[row_index],
|
||||
row_index,
|
||||
options,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
@@ -389,7 +487,8 @@ impl Render for DiffViewState {
|
||||
|
||||
fn render_row(
|
||||
row: &DiffRow,
|
||||
row_ix: usize,
|
||||
layout: &RowLayout,
|
||||
row_index: usize,
|
||||
options: DiffViewOptions,
|
||||
cx: &App,
|
||||
) -> gpui::AnyElement {
|
||||
@@ -402,8 +501,8 @@ fn render_row(
|
||||
let char_width = text_size * MONO_ADVANCE_RATIO;
|
||||
|
||||
let frame = div()
|
||||
.debug_selector(move || format!("diff-row-{row_ix}"))
|
||||
.id(("diff-row", row_ix))
|
||||
.debug_selector(move || format!("diff-row-{row_index}"))
|
||||
.id(layout.id.clone())
|
||||
.w_full()
|
||||
.h(line_height)
|
||||
.font_family(mono_font)
|
||||
@@ -496,7 +595,7 @@ fn render_row(
|
||||
&& *role != Role::Text
|
||||
{
|
||||
segment = segment.highlight(HighlightStyle {
|
||||
color: Some(role_color(*role, colors, ramps)),
|
||||
color: Some(role.color(colors, ramps)),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -582,7 +681,7 @@ fn render_row(
|
||||
div()
|
||||
.flex_none()
|
||||
.ml_2()
|
||||
.w(char_width * row.char_count() as f32)
|
||||
.w(char_width * layout.char_count as f32)
|
||||
.whitespace_nowrap()
|
||||
.child(gpui::StyledText::new(text).with_default_highlights(&style, highlights)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user