fix: diff search hit highlight across ranges

This commit is contained in:
2026-06-07 20:06:57 +01:00
parent be83a0b352
commit d09199a562
5 changed files with 198 additions and 35 deletions

View File

@@ -14,7 +14,7 @@ use crate::{
text_input::{self, TextInput, text_input}, text_input::{self, TextInput, text_input},
}, },
query::{self, QueryStatus, read_query, use_query, watch_query}, query::{self, QueryStatus, read_query, use_query, watch_query},
util::{self, diff::DiffLineIndex}, util::{self, diff::DiffLineIndex, syntax_highlight},
}; };
pub(crate) struct PullRequestDiffView { pub(crate) struct PullRequestDiffView {
@@ -42,6 +42,7 @@ struct DiffSearchResult {
cursor: DiffSearchResultCursor, cursor: DiffSearchResultCursor,
old_side: Vec<DiffSearchHit>, old_side: Vec<DiffSearchHit>,
new_side: Vec<DiffSearchHit>, new_side: Vec<DiffSearchHit>,
prev_diff_line_highlights: (usize, Vec<syntax_highlight::HighlightedRange>),
} }
struct DiffSearchResultCursor { struct DiffSearchResultCursor {
@@ -308,6 +309,7 @@ impl PullRequestDiffView {
cursor, cursor,
old_side: old_search_result, old_side: old_search_result,
new_side: new_search_result, new_side: new_search_result,
prev_diff_line_highlights: (0, Vec::new()),
}); });
} }
@@ -387,16 +389,23 @@ impl PullRequestDiffView {
if next_highlight_line == diff_line_i if next_highlight_line == diff_line_i
&& matches!(search_result.cursor.side, util::diff::DiffSide::Old) && matches!(search_result.cursor.side, util::diff::DiffSide::Old)
{ {
// there is a hit on new side on same diff line as the next hit on old side // next search hit candidates found on next diff line on both old side and new side
// go to old side first // since we are on new side currently, we should go back to old side first.
search_result.cursor.diff_line_i = diff_line_i; search_result.cursor.diff_line_i = diff_line_i;
search_result.cursor.index += 1; search_result.cursor.index += 1;
let (range, style) = &next.unwrap().highlight_range;
Some((
range.clone(),
gpui::HighlightStyle {
background_color: Some(gpui::red()),
..style.clone()
},
))
} else { } else {
// next cursor should be on other side with the found index // next cursor should be on other side with the found index
search_result.cursor.side = search_result.cursor.side.flipped(); search_result.cursor.side = search_result.cursor.side.flipped();
search_result.cursor.diff_line_i = diff_line_i; search_result.cursor.diff_line_i = diff_line_i;
search_result.cursor.index = other_side_i; search_result.cursor.index = other_side_i;
}
Some(( Some((
r.clone(), r.clone(),
gpui::HighlightStyle { gpui::HighlightStyle {
@@ -404,6 +413,7 @@ impl PullRequestDiffView {
..symbol_highlight_style(theme) ..symbol_highlight_style(theme)
}, },
)) ))
}
} else if let Some(next) = next { } else if let Some(next) = next {
// stay on old side, point to next old side highlight // stay on old side, point to next old side highlight
search_result.cursor.index = search_result.cursor.index + 1; search_result.cursor.index = search_result.cursor.index + 1;
@@ -426,8 +436,14 @@ impl PullRequestDiffView {
| util::diff::DiffSide::Old => diff_view_state.old_side_highlights_mut(), | util::diff::DiffSide::Old => diff_view_state.old_side_highlights_mut(),
| util::diff::DiffSide::New => diff_view_state.new_side_highlights_mut(), | util::diff::DiffSide::New => diff_view_state.new_side_highlights_mut(),
}; };
if let Some(mut content) = prev_highlighted_content { if let Some(mut content) = prev_highlighted_content
content.replace_highlight_range(&current_search_hit.highlight_range); && !search_result.prev_diff_line_highlights.1.is_empty()
{
// restore highlights for the line where the current higlighted search hit is in
content.set_highlights_for_line(
search_result.prev_diff_line_highlights.0,
search_result.prev_diff_line_highlights.1.drain(..),
);
} }
} }
@@ -435,8 +451,16 @@ impl PullRequestDiffView {
| util::diff::DiffSide::Old => diff_view_state.old_side_highlights_mut(), | util::diff::DiffSide::Old => diff_view_state.old_side_highlights_mut(),
| util::diff::DiffSide::New => diff_view_state.new_side_highlights_mut(), | util::diff::DiffSide::New => diff_view_state.new_side_highlights_mut(),
}; };
if let Some(mut content) = content {
content.replace_highlight_range(&highlight_range); if let Some(mut content) = content
&& let Some(line_i) = content.line_index_of_range(&highlight_range.0)
{
search_result.prev_diff_line_highlights.0 = line_i;
search_result
.prev_diff_line_highlights
.1
.splice(.., content.highlights_at_line(line_i).iter().cloned());
content.add_highlight_range(highlight_range);
} }
} }

View File

@@ -1,4 +1,4 @@
use std::{ops::Deref, sync::Arc}; use std::sync::Arc;
use similar::DiffableStr; use similar::DiffableStr;

View File

@@ -1,5 +1,6 @@
pub(crate) mod diff; pub(crate) mod diff;
pub(crate) mod file; pub(crate) mod file;
pub(crate) mod range;
pub(crate) mod str; pub(crate) mod str;
pub(crate) mod syntax_highlight; pub(crate) mod syntax_highlight;
pub(crate) mod timeout; pub(crate) mod timeout;

17
src/util/range.rs Normal file
View File

@@ -0,0 +1,17 @@
use std::ops::Sub;
pub(crate) trait RangeExt<T> {
fn relative_to(&self, other: &std::ops::Range<T>) -> std::ops::Range<T::Output>
where
T: Sub + Ord + PartialOrd + Clone + Copy;
}
impl<T> RangeExt<T> for std::ops::Range<T>
where
T: Sub + Ord + PartialOrd + Clone + Copy,
{
fn relative_to(&self, other: &std::ops::Range<T>) -> std::ops::Range<T::Output> {
debug_assert!(self.start >= other.start);
(self.start - other.start)..(self.end - other.start)
}
}

View File

@@ -1,6 +1,10 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::{component::code_view::symbol_highlight_style, theme, util}; use crate::{
component::{code_view::symbol_highlight_style, file_tree::Item},
theme,
util::{self, range::RangeExt, syntax_highlight},
};
pub(crate) type HighlightedRange = (std::ops::Range<usize>, gpui::HighlightStyle); pub(crate) type HighlightedRange = (std::ops::Range<usize>, gpui::HighlightStyle);
@@ -13,6 +17,68 @@ pub(crate) struct HighlightedLine {
highlights: Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>, highlights: Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>,
} }
fn highlight(a: gpui::HighlightStyle, b: gpui::HighlightStyle) -> gpui::HighlightStyle {
gpui::HighlightStyle {
color: b.color.or(a.color),
font_weight: b.font_weight.or(a.font_weight),
font_style: b.font_style.or(a.font_style),
background_color: b.background_color.or(a.background_color),
underline: b.underline.or(a.underline),
strikethrough: b.strikethrough.or(a.strikethrough),
fade_out: b.fade_out.or(a.fade_out),
}
}
fn combine_highlights(
a: impl IntoIterator<Item = HighlightedRange>,
b: impl IntoIterator<Item = HighlightedRange>,
) -> impl Iterator<Item = HighlightedRange> {
let mut endpoints = Vec::new();
let mut highlights = Vec::new();
for (range, highlight) in a.into_iter().chain(b) {
if !range.is_empty() {
let highlight_id = highlights.len();
endpoints.push((range.start, highlight_id, true));
endpoints.push((range.end, highlight_id, false));
highlights.push(highlight);
}
}
endpoints.sort_unstable_by_key(|(position, _, _)| *position);
let mut active = vec![false; highlights.len()];
let mut highlighted_ranges = Vec::new();
let mut previous_position = endpoints
.first()
.map(|(position, _, _)| *position)
.unwrap_or(0);
let mut endpoint_i = 0;
while endpoint_i < endpoints.len() {
let position = endpoints[endpoint_i].0;
if position > previous_position && active.iter().any(|active| *active) {
let style = highlights
.iter()
.enumerate()
.filter_map(|(highlight_id, style)| active[highlight_id].then_some(*style))
.fold(gpui::HighlightStyle::default(), highlight);
highlighted_ranges.push((previous_position..position, style));
}
while endpoint_i < endpoints.len() && endpoints[endpoint_i].0 == position {
let (_, highlight_id, is_start) = endpoints[endpoint_i];
active[highlight_id] = is_start;
endpoint_i += 1;
}
previous_position = position;
}
highlighted_ranges.into_iter()
}
static TS_HIGHLIGHTS: LazyLock<String> = LazyLock::new(|| { static TS_HIGHLIGHTS: LazyLock<String> = LazyLock::new(|| {
[ [
tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_javascript::HIGHLIGHT_QUERY,
@@ -209,7 +275,7 @@ impl HighlightedContent {
{ {
// moving on to new line, flush new highlights and combine into the line highlights // moving on to new line, flush new highlights and combine into the line highlights
if !new_highlights.is_empty() { if !new_highlights.is_empty() {
self.0[current_line].highlights = gpui::combine_highlights( self.0[current_line].highlights = combine_highlights(
self.0[current_line].highlights.drain(..).into_iter(), self.0[current_line].highlights.drain(..).into_iter(),
new_highlights.drain(..).into_iter(), new_highlights.drain(..).into_iter(),
) )
@@ -232,7 +298,7 @@ impl HighlightedContent {
} }
if !new_highlights.is_empty() { if !new_highlights.is_empty() {
self.0[current_line].highlights = gpui::combine_highlights( self.0[current_line].highlights = combine_highlights(
self.0[current_line].highlights.drain(..).into_iter(), self.0[current_line].highlights.drain(..).into_iter(),
new_highlights.drain(..).into_iter(), new_highlights.drain(..).into_iter(),
) )
@@ -242,6 +308,7 @@ impl HighlightedContent {
self self
} }
// given a range relative to the original source, returns the line number to which it belongs.
pub(crate) fn line_index_of_range(&self, range: &std::ops::Range<usize>) -> Option<usize> { pub(crate) fn line_index_of_range(&self, range: &std::ops::Range<usize>) -> Option<usize> {
self.0 self.0
.binary_search_by(|line| { .binary_search_by(|line| {
@@ -260,25 +327,79 @@ impl HighlightedContent {
.ok() .ok()
} }
pub(crate) fn replace_highlight_range(&mut self, highlighted_range: &HighlightedRange) { pub(crate) fn add_highlight_range(
let (range, _) = &highlighted_range; &mut self,
highlighted_range: HighlightedRange,
) -> Option<usize> {
let Some((line_i, line)) = self
.line_index_of_range(&highlighted_range.0)
.map(|line_i| (line_i, &mut self.0[line_i]))
else {
return None;
};
let line = self line.highlights = combine_highlights(
.line_index_of_range(range) line.highlights.drain(..).into_iter(),
.map(|line_i| &mut self.0[line_i]); std::iter::once((
highlighted_range.0.relative_to(&line.line_range),
highlighted_range.1,
)),
)
.collect();
let replace_idx = line.as_ref().and_then(|l| { Some(line_i)
let local_range = (range.start - l.line_range.start)..(range.end - l.line_range.start);
(l.highlights.iter().position(|(r, _)| *r == local_range)).map(|i| (i, local_range))
});
println!("line {:?} replace_idx {:?}", line, replace_idx);
match (line, replace_idx) {
| (Some(line), Some((replace_idx, local_range))) => {
line.highlights[replace_idx] = (local_range, highlighted_range.1)
} }
| _ => {}
pub(crate) fn set_highlights_for_line(
&mut self,
line_i: usize,
highlights: impl IntoIterator<Item = syntax_highlight::HighlightedRange>,
) {
self.0[line_i].highlights.splice(.., highlights);
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn background(color: gpui::Hsla) -> gpui::HighlightStyle {
gpui::HighlightStyle {
background_color: Some(color),
..Default::default()
}
}
#[test]
fn combine_highlights_gives_precedence_to_second_iterator() {
let combined = combine_highlights(
[(0..5, background(gpui::yellow()))],
[(0..5, background(gpui::red()))],
)
.collect::<Vec<_>>();
assert_eq!(combined, [(0..5, background(gpui::red()))]);
}
#[test]
fn combine_highlights_splits_ranges_and_keeps_second_iterator_precedence() {
let combined = combine_highlights(
[
(0..3, background(gpui::yellow())),
(3..6, background(gpui::green())),
],
[(1..5, background(gpui::red()))],
)
.collect::<Vec<_>>();
assert_eq!(
combined,
[
(0..1, background(gpui::yellow())),
(1..3, background(gpui::red())),
(3..5, background(gpui::red())),
(5..6, background(gpui::green())),
]
);
}
} }