refactor: hide logical comparison rows from painting
This commit is contained in:
+183
-7
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user