feat: add configurable diff modes and split orientation
This commit is contained in:
+185
-28
@@ -135,6 +135,13 @@ pub enum Focus {
|
||||
After,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ViewMode {
|
||||
Split,
|
||||
Unified,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PaneLayout {
|
||||
Split,
|
||||
@@ -179,6 +186,8 @@ pub struct DiffState {
|
||||
pub catalog: Option<Catalog>,
|
||||
pub phase: Phase,
|
||||
pub word_wrap: bool,
|
||||
pub view_mode: ViewMode,
|
||||
pub split_axis: crate::panes::SplitAxis,
|
||||
pub before: Option<Revision>,
|
||||
pub after: Option<Revision>,
|
||||
pub category: usize,
|
||||
@@ -198,6 +207,7 @@ pub struct DiffState {
|
||||
pub left_after: usize,
|
||||
pub rows: Vec<AlignedRow>,
|
||||
pub display_rows: Vec<DisplayRow>,
|
||||
display_sides: Vec<Option<Focus>>,
|
||||
excerpt_ranges: Vec<(usize, usize)>,
|
||||
pub live_buffer: Option<usize>,
|
||||
pub live_revision: Option<u64>,
|
||||
@@ -220,6 +230,8 @@ impl DiffState {
|
||||
catalog: None,
|
||||
phase: Phase::Before,
|
||||
word_wrap: false,
|
||||
view_mode: ViewMode::Split,
|
||||
split_axis: crate::panes::SplitAxis::Vertical,
|
||||
before: None,
|
||||
after: None,
|
||||
category: 0,
|
||||
@@ -239,6 +251,7 @@ impl DiffState {
|
||||
left_after: 0,
|
||||
rows: Vec::new(),
|
||||
display_rows: Vec::new(),
|
||||
display_sides: Vec::new(),
|
||||
excerpt_ranges: Vec::new(),
|
||||
live_buffer: None,
|
||||
live_revision: None,
|
||||
@@ -902,9 +915,40 @@ impl DiffState {
|
||||
}
|
||||
|
||||
pub(crate) fn source_display_row(&self, focus: Focus, source_row: usize) -> Option<usize> {
|
||||
self.display_rows.iter().position(|display| {
|
||||
matches!(*display, DisplayRow::Line(index) if self.source_row(focus, index) == Some(source_row))
|
||||
})
|
||||
(0..self.display_rows.len())
|
||||
.position(|display| self.display_source_row(focus, display) == Some(source_row))
|
||||
}
|
||||
|
||||
pub(crate) fn display_focus(&self, display: usize) -> Option<Focus> {
|
||||
self.display_sides.get(display).copied().flatten()
|
||||
}
|
||||
|
||||
pub(crate) fn display_source_row(&self, focus: Focus, display: usize) -> Option<usize> {
|
||||
if self
|
||||
.display_focus(display)
|
||||
.is_some_and(|side| side != focus)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
match *self.display_rows.get(display)? {
|
||||
DisplayRow::Line(index) => self.source_row(focus, index),
|
||||
DisplayRow::Fold { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_view_mode(&mut self, view_mode: ViewMode) {
|
||||
if self.view_mode == view_mode {
|
||||
return;
|
||||
}
|
||||
let anchor = self.display_rows.get(self.top).copied();
|
||||
let side = self.display_focus(self.top).or_else(|| {
|
||||
[self.focus, Focus::After, Focus::Before]
|
||||
.into_iter()
|
||||
.find(|&focus| self.display_source_row(focus, self.top).is_some())
|
||||
});
|
||||
self.view_mode = view_mode;
|
||||
self.rebuild_display_rows();
|
||||
self.restore_top(anchor, side);
|
||||
}
|
||||
|
||||
pub fn after_display_row(&self, source_row: usize) -> Option<usize> {
|
||||
@@ -951,9 +995,10 @@ impl DiffState {
|
||||
})?;
|
||||
self.display_rows
|
||||
.iter()
|
||||
.filter_map(|display| match *display {
|
||||
.enumerate()
|
||||
.filter_map(|(display, row)| match *row {
|
||||
DisplayRow::Line(index) => self
|
||||
.source_row(to, index)
|
||||
.display_source_row(to, display)
|
||||
.map(|row| (index.abs_diff(aligned), row)),
|
||||
DisplayRow::Fold { .. } => None,
|
||||
})
|
||||
@@ -975,7 +1020,7 @@ impl DiffState {
|
||||
previous
|
||||
}
|
||||
|
||||
fn restore_top(&mut self, anchor: Option<DisplayRow>) {
|
||||
fn restore_top(&mut self, anchor: Option<DisplayRow>, side: Option<Focus>) {
|
||||
if let Some(anchor) = anchor {
|
||||
let index = match anchor {
|
||||
DisplayRow::Line(index) => index,
|
||||
@@ -984,8 +1029,15 @@ impl DiffState {
|
||||
self.top = self
|
||||
.display_rows
|
||||
.iter()
|
||||
.position(|row| match *row {
|
||||
DisplayRow::Line(row) => row == index,
|
||||
.enumerate()
|
||||
.position(|(display, row)| match *row {
|
||||
DisplayRow::Line(row) => {
|
||||
row == index
|
||||
&& side.is_none_or(|side| {
|
||||
self.display_focus(display)
|
||||
.is_none_or(|focus| focus == side)
|
||||
})
|
||||
}
|
||||
DisplayRow::Fold { start, end } => start <= index && index < end,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
@@ -1044,20 +1096,19 @@ impl DiffState {
|
||||
after_visible.extend(row.after.map(map_after));
|
||||
}
|
||||
}
|
||||
let top_before = self
|
||||
let top_fold = self
|
||||
.display_rows
|
||||
.get(self.top)
|
||||
.and_then(|display| match *display {
|
||||
DisplayRow::Line(index) => self.rows[index].before,
|
||||
DisplayRow::Fold { start, .. } => self.rows[start].before,
|
||||
DisplayRow::Fold { start, .. } => Some(start),
|
||||
DisplayRow::Line(_) => None,
|
||||
});
|
||||
let top_before = self
|
||||
.display_source_row(Focus::Before, self.top)
|
||||
.or_else(|| top_fold.and_then(|index| self.rows[index].before));
|
||||
let top_after = self
|
||||
.display_rows
|
||||
.get(self.top)
|
||||
.and_then(|display| match *display {
|
||||
DisplayRow::Line(index) => self.rows[index].after,
|
||||
DisplayRow::Fold { start, .. } => self.rows[start].after,
|
||||
})
|
||||
.display_source_row(Focus::After, self.top)
|
||||
.or_else(|| top_fold.and_then(|index| self.rows[index].after))
|
||||
.map(map_after);
|
||||
let was_changed = file_is_changed(file);
|
||||
let file = &mut self.comparison.as_mut().unwrap().files[self.file];
|
||||
@@ -1102,7 +1153,7 @@ impl DiffState {
|
||||
} else if let Some(source) = top_before
|
||||
&& let Some(index) = self.rows.iter().position(|row| row.before == Some(source))
|
||||
{
|
||||
self.restore_top(Some(DisplayRow::Line(index)));
|
||||
self.restore_top(Some(DisplayRow::Line(index)), Some(Focus::Before));
|
||||
}
|
||||
if was_changed != changed {
|
||||
self.refresh_files();
|
||||
@@ -1134,6 +1185,7 @@ impl DiffState {
|
||||
self.normalize_focus();
|
||||
self.rows.clear();
|
||||
self.display_rows.clear();
|
||||
self.display_sides.clear();
|
||||
self.excerpt_ranges.clear();
|
||||
if reset {
|
||||
self.top = 0;
|
||||
@@ -1263,6 +1315,7 @@ impl DiffState {
|
||||
}
|
||||
self.excerpt_ranges.truncate(count);
|
||||
self.display_rows.clear();
|
||||
self.display_sides.clear();
|
||||
if self.excerpt_ranges.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -1273,9 +1326,34 @@ impl DiffState {
|
||||
start: end,
|
||||
end: start,
|
||||
});
|
||||
if self.view_mode == ViewMode::Unified {
|
||||
self.display_sides.push(None);
|
||||
}
|
||||
}
|
||||
let mut index = start;
|
||||
while index < next_end {
|
||||
if self.view_mode == ViewMode::Split || self.rows[index].kind == RowKind::Equal {
|
||||
self.display_rows.push(DisplayRow::Line(index));
|
||||
if self.view_mode == ViewMode::Unified {
|
||||
self.display_sides.push(None);
|
||||
}
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
let run_start = index;
|
||||
while index < next_end && self.rows[index].kind != RowKind::Equal {
|
||||
index += 1;
|
||||
}
|
||||
// Unified hunks show all removals before all additions, without alignment holes.
|
||||
for side in [Focus::Before, Focus::After] {
|
||||
for aligned in run_start..index {
|
||||
if self.source_row(side, aligned).is_some() {
|
||||
self.display_rows.push(DisplayRow::Line(aligned));
|
||||
self.display_sides.push(Some(side));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.display_rows
|
||||
.extend((start..next_end).map(DisplayRow::Line));
|
||||
end = next_end;
|
||||
}
|
||||
if end < self.rows.len() {
|
||||
@@ -1283,6 +1361,9 @@ impl DiffState {
|
||||
start: end,
|
||||
end: self.rows.len(),
|
||||
});
|
||||
if self.view_mode == ViewMode::Unified {
|
||||
self.display_sides.push(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,6 +1385,7 @@ impl DiffState {
|
||||
let Some(top) = self.display_rows.get(self.top).copied() else {
|
||||
return;
|
||||
};
|
||||
let top_side = self.display_focus(self.top);
|
||||
let index = match target {
|
||||
DisplayRow::Line(row) => self
|
||||
.excerpt_ranges
|
||||
@@ -1337,14 +1419,7 @@ impl DiffState {
|
||||
DisplayRow::Fold { start, .. } => start,
|
||||
};
|
||||
self.rebuild_display_rows();
|
||||
self.top = self
|
||||
.display_rows
|
||||
.iter()
|
||||
.position(|row| match *row {
|
||||
DisplayRow::Line(index) => index == anchor,
|
||||
DisplayRow::Fold { start, end } => start <= anchor && anchor < end,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
self.restore_top(Some(DisplayRow::Line(anchor)), top_side);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2804,6 +2879,88 @@ mod tests {
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_groups_changed_runs_and_roundtrips_side_specific_source_rows() {
|
||||
let mut state = live_state(
|
||||
["head", "old one", "old two", "tail"]
|
||||
.map(str::to_owned)
|
||||
.to_vec(),
|
||||
["head", "new one", "new two", "extra", "tail"]
|
||||
.map(str::to_owned)
|
||||
.to_vec(),
|
||||
false,
|
||||
);
|
||||
assert_eq!(state.view_mode, ViewMode::Split);
|
||||
assert_eq!(state.split_axis, crate::panes::SplitAxis::Vertical);
|
||||
state.top = state.after_display_row(2).unwrap();
|
||||
let excerpts = state.excerpt_ranges.clone();
|
||||
state.set_view_mode(ViewMode::Unified);
|
||||
let rows: Vec<_> = (0..state.display_rows.len())
|
||||
.map(|display| {
|
||||
(
|
||||
state.display_focus(display),
|
||||
state.display_source_row(Focus::Before, display),
|
||||
state.display_source_row(Focus::After, display),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
rows,
|
||||
[
|
||||
(None, Some(0), Some(0)),
|
||||
(Some(Focus::Before), Some(1), None),
|
||||
(Some(Focus::Before), Some(2), None),
|
||||
(Some(Focus::After), None, Some(1)),
|
||||
(Some(Focus::After), None, Some(2)),
|
||||
(Some(Focus::After), None, Some(3)),
|
||||
(None, Some(3), Some(4)),
|
||||
]
|
||||
);
|
||||
assert_eq!(state.top, state.after_display_row(2).unwrap());
|
||||
assert_eq!(
|
||||
state.corresponding_row(Focus::Before, Focus::After, 2),
|
||||
Some(2)
|
||||
);
|
||||
assert!(state.after_editable());
|
||||
for focus in [Focus::Before, Focus::After] {
|
||||
for source in state.source_rows(focus) {
|
||||
let display = state.source_display_row(focus, source).unwrap();
|
||||
assert_eq!(state.display_source_row(focus, display), Some(source));
|
||||
}
|
||||
}
|
||||
state.set_view_mode(ViewMode::Split);
|
||||
assert_eq!(state.excerpt_ranges, excerpts);
|
||||
assert_eq!(state.focus, Focus::After);
|
||||
assert_eq!(state.top, state.after_display_row(2).unwrap());
|
||||
assert_eq!(state.display_rows.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_live_reflow_preserves_removed_top_and_expanded_source_excerpts() {
|
||||
let base: Vec<_> = (0..40).map(|row| format!("line {row}")).collect();
|
||||
let mut lines = base.clone();
|
||||
lines[20] = "new line".into();
|
||||
let mut state = live_state(base, lines.clone(), false);
|
||||
state.set_context_options(1, 3);
|
||||
state.refresh_after(&lines, 1);
|
||||
state.set_view_mode(ViewMode::Unified);
|
||||
state.top = state.source_display_row(Focus::Before, 20).unwrap();
|
||||
state.expand_source_excerpt(Focus::After, false, 20);
|
||||
assert_eq!(state.display_source_row(Focus::Before, state.top), Some(20));
|
||||
assert!(state.source_display_row(Focus::After, 16).is_some());
|
||||
lines.insert(0, "inserted".into());
|
||||
state.refresh_after(&lines, 2);
|
||||
assert_eq!(state.display_focus(state.top), Some(Focus::Before));
|
||||
assert_eq!(state.display_source_row(Focus::Before, state.top), Some(20));
|
||||
assert!(state.source_display_row(Focus::After, 17).is_some());
|
||||
state.reveal_source_row(Focus::After, 35);
|
||||
let visible: Vec<_> = state.source_rows(Focus::After).collect();
|
||||
state.set_view_mode(ViewMode::Split);
|
||||
state.set_view_mode(ViewMode::Unified);
|
||||
assert_eq!(state.source_rows(Focus::After).collect::<Vec<_>>(), visible);
|
||||
assert_eq!(state.display_source_row(Focus::After, state.top), Some(35));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_ranges_follow_insertions_deletions_unicode_and_live_edits() {
|
||||
for (before, after, expected) in [
|
||||
|
||||
Reference in New Issue
Block a user