Fix bugs in diff hunk highlighting (#18454)
Fixes https://github.com/zed-industries/zed/issues/18405 In https://github.com/zed-industries/zed/pull/18313, we introduced a problem where git addition highlights might spuriously return when undoing certain changes. It turned out, there were already some cases where git hunk highlighting was incorrect when editing at the boundaries of expanded diff hunks. In this PR, I've introduced a test helper method for more rigorously (and readably) testing the editor's git state. You can assert about the entire state of an editor's diff decorations using a formatted diff: ```rust cx.assert_diff_hunks( r#" - use some::mod1; use some::mod2; const A: u32 = 42; - const B: u32 = 42; const C: u32 = 42; fn main() { - println!("hello"); + //println!("hello"); println!("world"); + // + // } fn another() { println!("another"); + println!("another"); } - fn another2() { println!("another2"); } "# .unindent(), ); ``` This will assert about the editor's actual row highlights, not just the editor's internal hunk-tracking state. I rewrote all of our editor diff tests to use these more high-level assertions, and it caught the new bug, as well as some pre-existing bugs in the highlighting of added content. The problem was how we *remove* highlighted rows. Previously, it relied on supplying exactly the same range as one that we had previously highlighted. I've added a `remove_highlighted_rows(ranges)` APIs which is much simpler - it clears out any row ranges that intersect the given ranges (which is all that we need for the Git diff use case). Release Notes: - N/A
This commit is contained in:
+119
-38
@@ -822,7 +822,7 @@ impl SelectionHistory {
|
||||
struct RowHighlight {
|
||||
index: usize,
|
||||
range: RangeInclusive<Anchor>,
|
||||
color: Option<Hsla>,
|
||||
color: Hsla,
|
||||
should_autoscroll: bool,
|
||||
}
|
||||
|
||||
@@ -11500,41 +11500,125 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds or removes (on `None` color) a highlight for the rows corresponding to the anchor range given.
|
||||
/// On matching anchor range, replaces the old highlight; does not clear the other existing highlights.
|
||||
/// If multiple anchor ranges will produce highlights for the same row, the last range added will be used.
|
||||
/// Adds a row highlight for the given range. If a row has multiple highlights, the
|
||||
/// last highlight added will be used.
|
||||
pub fn highlight_rows<T: 'static>(
|
||||
&mut self,
|
||||
rows: RangeInclusive<Anchor>,
|
||||
color: Option<Hsla>,
|
||||
range: RangeInclusive<Anchor>,
|
||||
color: Hsla,
|
||||
should_autoscroll: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let row_highlights = self.highlighted_rows.entry(TypeId::of::<T>()).or_default();
|
||||
let existing_highlight_index = row_highlights.binary_search_by(|highlight| {
|
||||
highlight
|
||||
.range
|
||||
.start()
|
||||
.cmp(rows.start(), &snapshot)
|
||||
.then(highlight.range.end().cmp(rows.end(), &snapshot))
|
||||
let ix = row_highlights.binary_search_by(|highlight| {
|
||||
Ordering::Equal
|
||||
.then_with(|| highlight.range.start().cmp(&range.start(), &snapshot))
|
||||
.then_with(|| highlight.range.end().cmp(&range.end(), &snapshot))
|
||||
});
|
||||
match (color, existing_highlight_index) {
|
||||
(Some(_), Ok(ix)) | (_, Err(ix)) => row_highlights.insert(
|
||||
ix,
|
||||
RowHighlight {
|
||||
index: post_inc(&mut self.highlight_order),
|
||||
range: rows,
|
||||
should_autoscroll,
|
||||
color,
|
||||
},
|
||||
),
|
||||
(None, Ok(i)) => {
|
||||
row_highlights.remove(i);
|
||||
|
||||
if let Err(mut ix) = ix {
|
||||
let index = post_inc(&mut self.highlight_order);
|
||||
|
||||
// If this range intersects with the preceding highlight, then merge it with
|
||||
// the preceding highlight. Otherwise insert a new highlight.
|
||||
let mut merged = false;
|
||||
if ix > 0 {
|
||||
let prev_highlight = &mut row_highlights[ix - 1];
|
||||
if prev_highlight
|
||||
.range
|
||||
.end()
|
||||
.cmp(&range.start(), &snapshot)
|
||||
.is_ge()
|
||||
{
|
||||
ix -= 1;
|
||||
if prev_highlight
|
||||
.range
|
||||
.end()
|
||||
.cmp(&range.end(), &snapshot)
|
||||
.is_lt()
|
||||
{
|
||||
prev_highlight.range = *prev_highlight.range.start()..=*range.end();
|
||||
}
|
||||
merged = true;
|
||||
prev_highlight.index = index;
|
||||
prev_highlight.color = color;
|
||||
prev_highlight.should_autoscroll = should_autoscroll;
|
||||
}
|
||||
}
|
||||
|
||||
if !merged {
|
||||
row_highlights.insert(
|
||||
ix,
|
||||
RowHighlight {
|
||||
range: range.clone(),
|
||||
index,
|
||||
color,
|
||||
should_autoscroll,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// If any of the following highlights intersect with this one, merge them.
|
||||
while let Some(next_highlight) = row_highlights.get(ix + 1) {
|
||||
let highlight = &row_highlights[ix];
|
||||
if next_highlight
|
||||
.range
|
||||
.start()
|
||||
.cmp(&highlight.range.end(), &snapshot)
|
||||
.is_le()
|
||||
{
|
||||
if next_highlight
|
||||
.range
|
||||
.end()
|
||||
.cmp(&highlight.range.end(), &snapshot)
|
||||
.is_gt()
|
||||
{
|
||||
row_highlights[ix].range =
|
||||
*highlight.range.start()..=*next_highlight.range.end();
|
||||
}
|
||||
row_highlights.remove(ix + 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove any highlighted row ranges of the given type that intersect the
|
||||
/// given ranges.
|
||||
pub fn remove_highlighted_rows<T: 'static>(
|
||||
&mut self,
|
||||
ranges_to_remove: Vec<Range<Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let row_highlights = self.highlighted_rows.entry(TypeId::of::<T>()).or_default();
|
||||
let mut ranges_to_remove = ranges_to_remove.iter().peekable();
|
||||
row_highlights.retain(|highlight| {
|
||||
while let Some(range_to_remove) = ranges_to_remove.peek() {
|
||||
match range_to_remove.end.cmp(&highlight.range.start(), &snapshot) {
|
||||
Ordering::Less => {
|
||||
ranges_to_remove.next();
|
||||
}
|
||||
Ordering::Equal => {
|
||||
return false;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
match range_to_remove.start.cmp(&highlight.range.end(), &snapshot) {
|
||||
Ordering::Less | Ordering::Equal => {
|
||||
return false;
|
||||
}
|
||||
Ordering::Greater => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted.
|
||||
pub fn clear_row_highlights<T: 'static>(&mut self) {
|
||||
self.highlighted_rows.remove(&TypeId::of::<T>());
|
||||
@@ -11543,13 +11627,12 @@ impl Editor {
|
||||
/// For a highlight given context type, gets all anchor ranges that will be used for row highlighting.
|
||||
pub fn highlighted_rows<T: 'static>(
|
||||
&self,
|
||||
) -> Option<impl Iterator<Item = (&RangeInclusive<Anchor>, Option<&Hsla>)>> {
|
||||
Some(
|
||||
self.highlighted_rows
|
||||
.get(&TypeId::of::<T>())?
|
||||
.iter()
|
||||
.map(|highlight| (&highlight.range, highlight.color.as_ref())),
|
||||
)
|
||||
) -> impl '_ + Iterator<Item = (RangeInclusive<Anchor>, Hsla)> {
|
||||
self.highlighted_rows
|
||||
.get(&TypeId::of::<T>())
|
||||
.map_or(&[] as &[_], |vec| vec.as_slice())
|
||||
.iter()
|
||||
.map(|highlight| (highlight.range.clone(), highlight.color))
|
||||
}
|
||||
|
||||
/// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict.
|
||||
@@ -11574,10 +11657,7 @@ impl Editor {
|
||||
used_highlight_orders.entry(row).or_insert(highlight.index);
|
||||
if highlight.index >= *used_index {
|
||||
*used_index = highlight.index;
|
||||
match highlight.color {
|
||||
Some(hsla) => unique_rows.insert(DisplayRow(row), hsla),
|
||||
None => unique_rows.remove(&DisplayRow(row)),
|
||||
};
|
||||
unique_rows.insert(DisplayRow(row), highlight.color);
|
||||
}
|
||||
}
|
||||
unique_rows
|
||||
@@ -11593,10 +11673,11 @@ impl Editor {
|
||||
.values()
|
||||
.flat_map(|highlighted_rows| highlighted_rows.iter())
|
||||
.filter_map(|highlight| {
|
||||
if highlight.color.is_none() || !highlight.should_autoscroll {
|
||||
return None;
|
||||
if highlight.should_autoscroll {
|
||||
Some(highlight.range.start().to_display_point(snapshot).row())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(highlight.range.start().to_display_point(snapshot).row())
|
||||
})
|
||||
.min()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user