refactor: hide logical comparison rows from painting

This commit is contained in:
Valdemar Lorenzen
2026-10-16 17:32:04 +02:00
parent a1fdc1e1b0
commit 4511bc2df9
5 changed files with 462 additions and 133 deletions
+183 -7
View File
@@ -240,6 +240,163 @@ pub enum DisplayRow {
Fold { start: usize, end: usize },
}
/// Borrowed painting and native-coordinate projection of a loaded comparison.
#[derive(Clone, Copy, Debug)]
pub(crate) struct ColumnView<'a> {
diff: &'a DiffState,
file: &'a ChangedFile,
pub info: ColumnInfo<'a>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ColumnInfo<'a> {
pub focus: Focus,
pub unified: bool,
pub display_len: usize,
pub source_extents: [usize; 2],
pub stored_lengths: [usize; 2],
pub empty: ColumnEmpty<'a>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ColumnEmpty<'a> {
EmptyFile,
NoChanges,
Notice(&'a str),
}
#[derive(Debug)]
pub(crate) enum ColumnRow<'a> {
Fold { hidden: usize },
Gap,
Text(ColumnLine<'a>),
}
#[derive(Debug)]
pub(crate) struct ColumnLine<'a> {
pub text: Option<&'a str>,
pub side: Focus,
pub source_row: usize,
pub numbers: [Option<usize>; 2],
pub changed: bool,
pub emphasize: bool,
pub newline: bool,
changes: &'a [[std::ops::Range<usize>; 2]],
}
impl<'a> ColumnLine<'a> {
pub fn changes(&self) -> impl Iterator<Item = &'a std::ops::Range<usize>> + 'a {
let side = usize::from(self.side == Focus::After);
let emphasize = self.emphasize;
self.changes
.iter()
.filter(move |_| emphasize)
.map(move |change| &change[side])
}
}
impl<'a> ColumnView<'a> {
fn new(diff: &'a DiffState, file: &'a ChangedFile, focus: Focus) -> Self {
let stored_lengths = [file.before.len(), file.after.len()];
let last = diff.rows.last();
let source_extents = [
stored_lengths[0].max(last.and_then(|row| row.before).map_or(0, |row| row + 1)),
stored_lengths[1].max(last.and_then(|row| row.after).map_or(0, |row| row + 1)),
];
Self {
diff,
file,
info: ColumnInfo {
focus,
unified: diff.view_mode == ViewMode::Unified,
display_len: diff.display_rows.len(),
source_extents,
stored_lengths,
empty: match file.notice.as_deref() {
Some(notice) => ColumnEmpty::Notice(notice),
None if diff.rows.is_empty() => ColumnEmpty::EmptyFile,
None => ColumnEmpty::NoChanges,
},
},
}
}
pub fn row(&self, display: usize) -> Option<ColumnRow<'a>> {
let index = match *self.diff.display_rows.get(display)? {
DisplayRow::Fold { start, end } => {
return Some(ColumnRow::Fold {
hidden: end - start,
});
}
DisplayRow::Line(index) => index,
};
let row = self.diff.rows.get(index)?;
let side = if self.info.unified {
self.diff.display_focus(display).unwrap_or(self.info.focus)
} else {
self.info.focus
};
let Some(source_row) = self.diff.display_source_row(side, display) else {
return Some(ColumnRow::Gap);
};
let before = side == Focus::Before;
let side_index = usize::from(!before);
let (source, eof) = if before {
(&self.file.before, self.file.before_eof)
} else {
(&self.file.after, self.file.after_eof)
};
let text = source
.get(source_row)
.map(String::as_str)
.or_else(|| (self.diff.terminal_rows[side_index] == Some(source_row)).then_some(""));
Some(ColumnRow::Text(ColumnLine {
text,
side,
source_row,
numbers: [
self.diff.display_source_row(Focus::Before, display),
self.diff.display_source_row(Focus::After, display),
],
changed: row.kind == RowKind::Changed
|| (before && row.kind == RowKind::Removed)
|| (!before && row.kind == RowKind::Added),
emphasize: !matches!(self.file.status, FileStatus::Added | FileStatus::Removed),
newline: source_row + 1 < source.len() || eof,
changes: &row.changes,
}))
}
/// Caret mapping stays on this view's native side, even in unified display.
pub fn native_row(&self, display: usize) -> Option<usize> {
self.diff.display_source_row(self.info.focus, display)
}
/// Finds the first visible native match; never reveals folded source rows.
pub fn display_row(&self, native: usize) -> Option<usize> {
self.diff.source_display_row(self.info.focus, native)
}
/// Review anchors may address either coordinate of unified equal context,
/// independently of the focused native caret mapping.
pub fn review_row(&self, side: Focus, display: usize) -> Option<usize> {
self.diff.display_source_row(side, display).or_else(|| {
let DisplayRow::Line(index) = *self.diff.display_rows.get(display)? else {
return None;
};
let row = self.diff.rows.get(index)?;
if !self.info.unified || row.kind != RowKind::Equal {
return None;
}
match side {
Focus::Before => row.before,
Focus::After => row.after,
Focus::Files => None,
}
})
}
}
/// Immutable left/base/right conflict History, aligned at shared base rows.
pub(crate) struct HistoryComparison {
columns: [DiffState; 3],
@@ -253,13 +410,13 @@ pub(crate) struct HistoryColumn<'a> {
pub lines: &'a [String],
}
impl HistoryColumn<'_> {
pub fn source_row(&self, display: usize) -> Option<usize> {
self.diff.display_source_row(self.focus, display)
impl<'a> HistoryColumn<'a> {
pub fn view(&self) -> ColumnView<'a> {
ColumnView::new(self.diff, self.file, self.focus)
}
pub fn display_row(&self, source: usize) -> Option<usize> {
self.diff.source_display_row(self.focus, source)
pub fn source_row(&self, display: usize) -> Option<usize> {
self.diff.display_source_row(self.focus, display)
}
}
@@ -404,6 +561,8 @@ pub struct DiffState {
pub rows: Vec<AlignedRow>,
pub display_rows: Vec<DisplayRow>,
display_sides: Vec<Option<Focus>>,
/// Native empty terminal rows explicitly supplied by resolver text.
terminal_rows: [Option<usize>; 2],
excerpt_ranges: Vec<(usize, usize)>,
pub live_buffer: Option<usize>,
pub live_revision: Option<u64>,
@@ -462,6 +621,7 @@ impl DiffState {
rows: Vec::new(),
display_rows: Vec::new(),
display_sides: Vec::new(),
terminal_rows: [None; 2],
excerpt_ranges: Vec::new(),
live_buffer: None,
live_revision: None,
@@ -538,7 +698,10 @@ impl DiffState {
}
let (before, before_eof) = content(draft);
let (after, after_eof) = content(alternative);
let before_terminal = (draft.len() > before.len()).then_some(before.len());
let after_terminal = (alternative.len() > after.len()).then_some(after.len());
if self.phase == Phase::View
&& self.terminal_rows == [before_terminal, after_terminal]
&& self.comparison.as_ref().is_some_and(|comparison| {
let file = &comparison.files[self.file];
file.before == before
@@ -567,8 +730,7 @@ impl DiffState {
file.after_eof = after_eof;
file.hunks = line_hunks(before, before_eof, after, after_eof);
self.align_file();
let before_terminal = (draft.len() > before.len()).then_some(before.len());
let after_terminal = (alternative.len() > after.len()).then_some(after.len());
self.terminal_rows = [before_terminal, after_terminal];
if before_terminal.is_some() || after_terminal.is_some() {
self.rows.push(AlignedRow {
before: before_terminal,
@@ -1650,6 +1812,19 @@ impl DiffState {
.is_some_and(|file| file.loaded)
}
pub(crate) fn column_view(&self, focus: Focus) -> Option<ColumnView<'_>> {
if focus == Focus::Files {
return None;
}
let file = self
.comparison
.as_ref()?
.files
.get(self.file)
.filter(|file| file.loaded)?;
Some(ColumnView::new(self, file, focus))
}
/// Source rows without the editor's virtual terminal row, preserving real empty lines.
pub(crate) fn after_source_lines<'a>(&self, file: &'a ChangedFile) -> &'a [String] {
if self.live_files.contains(&self.file) {
@@ -2079,6 +2254,7 @@ impl DiffState {
fn rebuild_file(&mut self, reset: bool) {
self.normalize_focus();
self.rows.clear();
self.terminal_rows = [None; 2];
self.display_rows.clear();
self.display_sides.clear();
self.excerpt_ranges.clear();