fix: coalesce fragmented word-diff highlights
This commit is contained in:
@@ -4,7 +4,8 @@
|
|||||||
- it should be taller
|
- it should be taller
|
||||||
- [x] add support for zig syntax highlighting
|
- [x] add support for zig syntax highlighting
|
||||||
- [x] add the `*` action from vim on word to highlights and searches in buffer in one action.
|
- [x] add the `*` action from vim on word to highlights and searches in buffer in one action.
|
||||||
- [ ] improve diff view: we should be able to tell what the part of the code we're looking at (any given change hunk) and where it belongs.
|
- [x] diff view: improve word-diff highlighting
|
||||||
|
- [ ] diff view: we should be able to tell what the part of the code we're looking at (any given change hunk) and where it belongs.
|
||||||
- i think it would be nice if we in the diff view somehow displayed the function name and/or class name that a hunk belongs to
|
- i think it would be nice if we in the diff view somehow displayed the function name and/or class name that a hunk belongs to
|
||||||
- we should consider the ui before implementing this
|
- we should consider the ui before implementing this
|
||||||
- [ ] diff view: github integration (code review) - add comments on a pr, add review comments at specific lines of files, request changes, approve pr, etc.
|
- [ ] diff view: github integration (code review) - add comments on a pr, add review comments at specific lines of files, request changes, approve pr, etc.
|
||||||
|
|||||||
+1
-1
@@ -452,7 +452,7 @@ In split diffs, changed text rows use muted red backgrounds in Before and muted
|
|||||||
|
|
||||||
The row tint also covers the line-number gutter on wrapped continuations and blank wrap-padding rows on the shorter side of an aligned pair. Empty source lines keep the same full-row tint; missing source counterparts retain their normal-background hatching.
|
The row tint also covers the line-number gutter on wrapped continuations and blank wrap-padding rows on the shorter side of an aligned pair. Empty source lines keep the same full-row tint; missing source counterparts retain their normal-background hatching.
|
||||||
|
|
||||||
Character-wise highlighting is enabled automatically within paired changed lines: only changed graphemes receive a stronger red/green background, while common text retains the muted row tint. Combining sequences, wide characters, and expanded tabs keep their source geometry through horizontal scrolling and word wrap. Syntax/semantic foregrounds remain intact, and cursor/search/selection styles keep their normal precedence. Character ranges are cached with aligned rows; stable live edits refresh only the edited paired lines rather than computing character diffs during painting.
|
Character-wise highlighting is enabled automatically within paired changed lines, with a stronger red/green background over changed grapheme spans. To reduce fragmented highlighting, changes separated by at most two unchanged graphemes are merged; short unchanged islands between large edits are also absorbed using VS Code-style size heuristics. Larger common regions retain the muted row tint. Combining sequences, wide characters, and expanded tabs keep their source geometry through horizontal scrolling and word wrap. Syntax/semantic foregrounds remain intact, and cursor/search/selection styles keep their normal precedence. Character ranges are cached with aligned rows; stable live edits refresh only the edited paired lines rather than computing character diffs during painting.
|
||||||
|
|
||||||
Diff backgrounds blend the terminal's default background with its red/green palette colors: an 18% accent for changed rows and a stronger 38% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint, on focus regain, and whenever a subscribed DEC 2031 color-scheme notification arrives. While a diff is visible in any pane, a nonblocking one-second palette probe also catches external theme changes without a notification, including changes between two themes with the same light/dark preference. Ordinary editor views do not poll; cached colors apply across editor panes. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.
|
Diff backgrounds blend the terminal's default background with its red/green palette colors: an 18% accent for changed rows and a stronger 38% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint, on focus regain, and whenever a subscribed DEC 2031 color-scheme notification arrives. While a diff is visible in any pane, a nonblocking one-second palette probe also catches external theme changes without a notification, including changes between two themes with the same light/dark preference. Ordinary editor views do not poll; cached colors apply across editor panes. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.
|
||||||
|
|
||||||
|
|||||||
+126
-5
@@ -2241,13 +2241,83 @@ fn character_changes(before: &str, after: &str) -> Vec<[std::ops::Range<usize>;
|
|||||||
}
|
}
|
||||||
let before: Vec<_> = before.graphemes(true).collect();
|
let before: Vec<_> = before.graphemes(true).collect();
|
||||||
let after: Vec<_> = after.graphemes(true).collect();
|
let after: Vec<_> = after.graphemes(true).collect();
|
||||||
similar::capture_diff_slices(similar::Algorithm::Myers, &before, &after)
|
let mut changes: Vec<[std::ops::Range<usize>; 2]> = Vec::new();
|
||||||
.into_iter()
|
let (mut before_offset, mut after_offset) = (0, 0);
|
||||||
.filter_map(|op| {
|
for op in similar::capture_diff_slices(similar::Algorithm::Myers, &before, &after) {
|
||||||
let (tag, before, after) = op.as_tag_tuple();
|
let (tag, before, after) = op.as_tag_tuple();
|
||||||
(tag != similar::DiffTag::Equal).then_some([before, after])
|
// similar's optimized empty-range anchors can be stale; consume lengths in order.
|
||||||
|
let before = before_offset..before_offset + before.len();
|
||||||
|
let after = after_offset..after_offset + after.len();
|
||||||
|
(before_offset, after_offset) = (before.end, after.end);
|
||||||
|
if tag == similar::DiffTag::Equal {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(previous) = changes.last_mut()
|
||||||
|
&& (before.start - previous[0].end <= 2 || after.start - previous[1].end <= 2)
|
||||||
|
{
|
||||||
|
previous[0].end = before.end;
|
||||||
|
previous[1].end = after.end;
|
||||||
|
} else {
|
||||||
|
changes.push([before, after]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A merge can enable an earlier gap; VS Code bounds this pass to 11 sweeps.
|
||||||
|
for _ in 0..11 {
|
||||||
|
let mut kept = 1;
|
||||||
|
for index in 1..changes.len() {
|
||||||
|
if short_match_between_large_changes(&before, &changes[kept - 1], &changes[index]) {
|
||||||
|
changes[kept - 1][0].end = changes[index][0].end;
|
||||||
|
changes[kept - 1][1].end = changes[index][1].end;
|
||||||
|
} else {
|
||||||
|
if kept != index {
|
||||||
|
changes[kept] = changes[index].clone();
|
||||||
|
}
|
||||||
|
kept += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kept >= changes.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
changes.truncate(kept);
|
||||||
|
}
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short_match_between_large_changes(
|
||||||
|
before: &[&str],
|
||||||
|
previous: &[std::ops::Range<usize>; 2],
|
||||||
|
next: &[std::ops::Range<usize>; 2],
|
||||||
|
) -> bool {
|
||||||
|
let gap = &before[previous[0].end..next[0].start];
|
||||||
|
if gap.len() > 500 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let start = gap
|
||||||
|
.iter()
|
||||||
|
.position(|grapheme| !grapheme.trim().is_empty())
|
||||||
|
.unwrap_or(gap.len());
|
||||||
|
let trimmed_len = gap[start..]
|
||||||
|
.iter()
|
||||||
|
.rposition(|grapheme| !grapheme.trim().is_empty())
|
||||||
|
.map_or(0, |end| end + 1);
|
||||||
|
if trimmed_len > 20 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// VS Code's size heuristic, specialized to single lines and normalized by 130^2.25:
|
||||||
|
// https://github.com/microsoft/vscode/blob/main/src/vs/editor/common/diff/defaultLinesDiffComputer/heuristicSequenceOptimizations.ts
|
||||||
|
let score = |change: &[std::ops::Range<usize>; 2]| {
|
||||||
|
let weight = change
|
||||||
|
.iter()
|
||||||
|
.map(|range| {
|
||||||
|
let length = range.len().min(130) as f64 / 130.0;
|
||||||
|
length * length.sqrt()
|
||||||
})
|
})
|
||||||
.collect()
|
.sum::<f64>();
|
||||||
|
weight * weight.sqrt()
|
||||||
|
};
|
||||||
|
score(previous) + score(next) > 1.3
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_width(line: &str) -> usize {
|
fn line_width(line: &str) -> usize {
|
||||||
@@ -4522,6 +4592,57 @@ mod tests {
|
|||||||
assert!(state.rows[0].changes.is_empty());
|
assert!(state.rows[0].changes.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn character_highlights_merge_incidental_matches_but_preserve_meaningful_gaps() {
|
||||||
|
let mut cases = vec![
|
||||||
|
("a..c".into(), "b..d".into(), vec![[0..4, 0..4]]),
|
||||||
|
("base".into(), "edited".into(), vec![[0..4, 0..6]]),
|
||||||
|
(
|
||||||
|
"a...c".into(),
|
||||||
|
"b...d".into(),
|
||||||
|
vec![[0..1, 0..1], [4..5, 4..5]],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a界e\u{301}c".into(),
|
||||||
|
"b界e\u{301}d".into(),
|
||||||
|
vec![[0..4, 0..4]],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (size, gap, merge) in [
|
||||||
|
(60, " = ".to_owned(), false),
|
||||||
|
(70, " = ".to_owned(), true),
|
||||||
|
(70, "x".repeat(20), true),
|
||||||
|
(70, "unchanged_expression".to_owned(), true),
|
||||||
|
(70, "unchanged_expression_".to_owned(), false),
|
||||||
|
(70, " ".repeat(500), true),
|
||||||
|
(70, " ".repeat(501), false),
|
||||||
|
] {
|
||||||
|
let end = 2 * size + gap.len();
|
||||||
|
let second = size + gap.len();
|
||||||
|
cases.push((
|
||||||
|
format!("{}{}{}", "a".repeat(size), gap, "c".repeat(size)),
|
||||||
|
format!("{}{}{}", "b".repeat(size), gap, "d".repeat(size)),
|
||||||
|
if merge {
|
||||||
|
vec![[0..end, 0..end]]
|
||||||
|
} else {
|
||||||
|
vec![[0..size, 0..size], [second..end, second..end]]
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
cases.push((
|
||||||
|
format!("aaa = {} = {}", "c".repeat(70), "e".repeat(70)),
|
||||||
|
format!("bbb = {} = {}", "d".repeat(70), "f".repeat(70)),
|
||||||
|
vec![[0..149, 0..149]],
|
||||||
|
));
|
||||||
|
for (before, after, expected) in cases {
|
||||||
|
let mut state = live_state(vec![before], vec![after.clone()], false);
|
||||||
|
assert_eq!(state.rows[0].changes, expected, "{after:?}");
|
||||||
|
state.refresh_after(&[format!("{after}!")], 1);
|
||||||
|
state.refresh_after(std::slice::from_ref(&after), 2);
|
||||||
|
assert_eq!(state.rows[0].changes, expected, "live edit: {after:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reverting_loaded_file_keeps_unloaded_changes_selectable() {
|
fn reverting_loaded_file_keeps_unloaded_changes_selectable() {
|
||||||
let mut state = live_state(vec!["base".into()], vec!["changed".into()], false);
|
let mut state = live_state(vec!["base".into()], vec!["changed".into()], false);
|
||||||
|
|||||||
Reference in New Issue
Block a user