feat: highlight character-level changes in diff panes
This commit is contained in:
+59
@@ -154,6 +154,8 @@ pub struct AlignedRow {
|
||||
pub before: Option<usize>,
|
||||
pub after: Option<usize>,
|
||||
pub kind: RowKind,
|
||||
/// Changed grapheme ranges, paired as [before, after]; empty on unpaired rows.
|
||||
pub changes: Vec<[std::ops::Range<usize>; 2]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -958,6 +960,7 @@ impl DiffState {
|
||||
lines,
|
||||
changed_lines,
|
||||
);
|
||||
refresh_character_changes(&mut self.rows, file, changed_lines.0..changed_lines.2);
|
||||
self.left_after = self.left_after.min(self.max_after);
|
||||
self.live_revision = Some(revision);
|
||||
return;
|
||||
@@ -1150,8 +1153,10 @@ impl DiffState {
|
||||
before: None,
|
||||
after: Some(after_len),
|
||||
kind: RowKind::Equal,
|
||||
changes: Vec::new(),
|
||||
});
|
||||
}
|
||||
refresh_character_changes(&mut self.rows, file, 0..file.after.len());
|
||||
self.reset_excerpts();
|
||||
}
|
||||
|
||||
@@ -1310,10 +1315,43 @@ fn append_rows(
|
||||
before,
|
||||
after,
|
||||
kind,
|
||||
changes: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_character_changes(
|
||||
rows: &mut [AlignedRow],
|
||||
file: &ChangedFile,
|
||||
after_rows: std::ops::Range<usize>,
|
||||
) {
|
||||
for row in rows {
|
||||
if row.kind == RowKind::Changed
|
||||
&& let Some(after) = row.after.filter(|index| after_rows.contains(index))
|
||||
{
|
||||
row.changes = character_changes(
|
||||
&file.before[row.before.expect("paired changed row")],
|
||||
&file.after[after],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn character_changes(before: &str, after: &str) -> Vec<[std::ops::Range<usize>; 2]> {
|
||||
if before == after {
|
||||
return Vec::new();
|
||||
}
|
||||
let before: Vec<_> = before.graphemes(true).collect();
|
||||
let after: Vec<_> = after.graphemes(true).collect();
|
||||
similar::capture_diff_slices(similar::Algorithm::Myers, &before, &after)
|
||||
.into_iter()
|
||||
.filter_map(|op| {
|
||||
let (tag, before, after) = op.as_tag_tuple();
|
||||
(tag != similar::DiffTag::Equal).then_some([before, after])
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn line_width(line: &str) -> usize {
|
||||
#[cfg(test)]
|
||||
tests::WIDTH_SCANS.with(|scans| scans.set(scans.get() + 1));
|
||||
@@ -1502,6 +1540,7 @@ mod tests {
|
||||
} else {
|
||||
RowKind::Equal
|
||||
},
|
||||
changes: Vec::new(),
|
||||
})
|
||||
.collect();
|
||||
state.reset_excerpts();
|
||||
@@ -2631,6 +2670,26 @@ mod tests {
|
||||
state
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_ranges_follow_insertions_deletions_unicode_and_live_edits() {
|
||||
for (before, after, expected) in [
|
||||
("ab", "a界b", vec![[1..1, 1..2]]),
|
||||
("a界b", "ab", vec![[1..2, 1..1]]),
|
||||
("xe\u{301}👩🚀z", "xe\u{300}👩🔬z", vec![[1..3, 1..3]]),
|
||||
] {
|
||||
let state = live_state(vec![before.into()], vec![after.into()], false);
|
||||
assert_eq!(state.rows[0].changes, expected);
|
||||
}
|
||||
let mut state = live_state(vec!["abc".into()], vec!["aXbc".into()], false);
|
||||
state.refresh_after(&["aXYbc".into()], 1);
|
||||
assert_eq!(state.rows[0].changes, vec![[1..1, 1..3]]);
|
||||
state.refresh_after(&["abYZc".into()], 2);
|
||||
assert_eq!(state.rows[0].changes, vec![[2..2, 2..4]]);
|
||||
state.refresh_after(&["abc".into()], 3);
|
||||
assert_eq!(state.rows[0].kind, RowKind::Equal);
|
||||
assert!(state.rows[0].changes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverting_loaded_file_keeps_unloaded_changes_selectable() {
|
||||
let mut state = live_state(vec!["base".into()], vec!["changed".into()], false);
|
||||
|
||||
Reference in New Issue
Block a user