feat: search result highlight in pr diff view

This commit is contained in:
2026-06-01 01:15:47 +01:00
parent f5ebb210ac
commit 240d48ff1e
4 changed files with 488 additions and 80 deletions

View File

@@ -1,21 +1,30 @@
use crate::{theme, util};
use crate::{component::code_view::symbol_highlight_style, theme, util};
pub(crate) struct HighlightedContent(Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>>);
pub(crate) type HighlightedRange = (std::ops::Range<usize>, gpui::HighlightStyle);
#[derive(Clone)]
pub(crate) struct HighlightedContent(Vec<HighlightedLine>);
#[derive(Default, Clone, Debug)]
pub(crate) struct HighlightedLine {
line_range: std::ops::Range<usize>,
highlights: Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>,
}
fn ts_highlight_configuration_for_file_type(
file_type: util::file::FileType,
) -> Option<tree_sitter_highlight::HighlightConfiguration> {
match file_type {
| util::file::FileType::Rust => tree_sitter_highlight::HighlightConfiguration::new(
tree_sitter_rust::LANGUAGE.into(),
"rust",
tree_sitter_rust::HIGHLIGHTS_QUERY,
tree_sitter_rust::INJECTIONS_QUERY,
"",
)
.ok(),
| util::file::FileType::Rust => tree_sitter_highlight::HighlightConfiguration::new(
tree_sitter_rust::LANGUAGE.into(),
"rust",
tree_sitter_rust::HIGHLIGHTS_QUERY,
tree_sitter_rust::INJECTIONS_QUERY,
"",
)
.ok(),
| _ => None,
| _ => None,
}
}
@@ -38,43 +47,63 @@ pub(crate) fn highlight_content(
let line_ranges = util::file::line_ranges(content.as_ref());
let mut highlights: Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>> =
Vec::with_capacity(line_ranges.len());
let mut highlights: Vec<HighlightedLine> = Vec::with_capacity(line_ranges.len());
let mut current_line: usize = 0;
// Tree-sitter produces a flat event stream:
//
// HighlightStart(style) -> Source { start, end } -> HighlightEnd
//
// `highlight` tracks the currently active style while each Source span is
// converted from absolute byte offsets into per-line, line-relative ranges.
// Empty lines before the span are emitted as empty highlight lists, then a
// span that crosses line boundaries is clipped into one range per touched
// line:
//
// bytes: [ line 0 ][ line 1 ][ line 2 ]
// span: [===========)
// out: [--] [------] [--]
for highlight_event in events {
match highlight_event.ok()? {
| tree_sitter_highlight::HighlightEvent::HighlightStart(h) => {
highlight = theme_syntax.as_slice()[h.0];
}
| tree_sitter_highlight::HighlightEvent::Source { start, end } => {
while current_line < line_ranges.len() && start >= line_ranges[current_line].end {
highlights.push(Vec::new());
current_line += 1;
| tree_sitter_highlight::HighlightEvent::HighlightStart(h) => {
highlight = theme_syntax.as_slice()[h.0];
}
| tree_sitter_highlight::HighlightEvent::Source { start, end } => {
while current_line < line_ranges.len() && start >= line_ranges[current_line].end {
if highlights.get(current_line).is_none() {
highlights.push(HighlightedLine {
line_range: line_ranges[current_line].clone(),
highlights: Vec::new(),
});
}
let mut line = current_line;
while line < line_ranges.len() && end > line_ranges[line].start {
if highlights.get(line).is_none() {
highlights.push(Vec::new());
}
let line_range = &line_ranges[line];
let highlight_start = start.max(line_range.start);
let highlight_end = end.min(line_range.end);
highlights[line].push((
(highlight_start - line_range.start)..(highlight_end - line_range.start),
gpui::HighlightStyle {
color: Some(highlight.color.into()),
font_weight: highlight.font_weight,
font_style: Some(highlight.font_style),
..Default::default()
},
));
line += 1;
current_line += 1;
}
let mut line = current_line;
while line < line_ranges.len() && end > line_ranges[line].start {
let line_range = &line_ranges[line];
if highlights.get(line).is_none() {
highlights.push(HighlightedLine {
line_range: line_range.clone(),
highlights: Vec::new(),
});
}
let highlight_start = start.max(line_range.start);
let highlight_end = end.min(line_range.end);
highlights[line].highlights.push((
(highlight_start - line_range.start)..(highlight_end - line_range.start),
gpui::HighlightStyle {
color: Some(highlight.color.into()),
font_weight: highlight.font_weight,
font_style: Some(highlight.font_style),
..Default::default()
},
));
line += 1;
}
| tree_sitter_highlight::HighlightEvent::HighlightEnd => {
highlight = default_highlight;
}
}
| tree_sitter_highlight::HighlightEvent::HighlightEnd => {
highlight = default_highlight;
}
}
}
@@ -85,7 +114,101 @@ impl HighlightedContent {
pub(crate) fn highlights_at_line(
&self,
line: usize,
) -> &Vec<(std::ops::Range<usize>, gpui::HighlightStyle)> {
&self.0[line]
) -> &[(std::ops::Range<usize>, gpui::HighlightStyle)] {
&self.0[line].highlights
}
pub(crate) fn line_range(&self, line: usize) -> &std::ops::Range<usize> {
&self.0[line].line_range
}
pub(crate) fn extended_with_highlights(
mut self,
highlights: impl Iterator<Item = HighlightedRange>,
) -> Self {
let total_line_count = self.0.len();
let mut current_line: usize = 0;
let mut new_highlights: Vec<HighlightedRange> = Vec::new();
for (range, highlight) in highlights {
// move to next line until the highlight range start is within bounds of a line (ie range start is before line range end)
while current_line < total_line_count
&& range.start >= self.line_range(current_line).end
{
// moving on to new line, flush new highlights and combine into the line highlights
if !new_highlights.is_empty() {
self.0[current_line].highlights = gpui::combine_highlights(
self.0[current_line].highlights.drain(..).into_iter(),
new_highlights.drain(..).into_iter(),
)
.collect();
}
current_line += 1;
}
let mut line = current_line;
while line < total_line_count && range.end > self.line_range(line).start {
let highlight_start = range.start.max(self.line_range(line).start);
let highlight_end = range.end.min(self.line_range(line).end);
new_highlights.push((
(highlight_start - self.line_range(line).start)
..(highlight_end - self.line_range(line).start),
highlight.clone(),
));
line += 1;
}
}
if !new_highlights.is_empty() {
self.0[current_line].highlights = gpui::combine_highlights(
self.0[current_line].highlights.drain(..).into_iter(),
new_highlights.drain(..).into_iter(),
)
.collect();
}
self
}
pub(crate) fn line_index_of_range(&self, range: &std::ops::Range<usize>) -> Option<usize> {
self.0
.binary_search_by(|line| {
let line_start = line.line_range.start;
let line_end = line.line_range.end;
if (range.start <= line_start && range.end >= line_end)
|| (line_start <= range.start && line_end >= range.end)
{
std::cmp::Ordering::Equal
} else if range.start < line_start && range.end < line_end {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less
}
})
.ok()
}
pub(crate) fn replace_highlight_range(&mut self, highlighted_range: &HighlightedRange) {
let (range, _) = &highlighted_range;
println!("finding range to replace {:?}", range);
let line = self
.line_index_of_range(range)
.map(|line_i| &mut self.0[line_i]);
let replace_idx = line.as_ref().and_then(|l| {
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)
}
| _ => {}
}
}
}