diff --git a/src/screen/dashboard/pull_request_diff_view.rs b/src/screen/dashboard/pull_request_diff_view.rs index 0829897..5860eda 100644 --- a/src/screen/dashboard/pull_request_diff_view.rs +++ b/src/screen/dashboard/pull_request_diff_view.rs @@ -14,7 +14,7 @@ use crate::{ text_input::{self, TextInput, text_input}, }, query::{self, QueryStatus, read_query, use_query, watch_query}, - util::{self, diff::DiffLineIndex}, + util::{self, diff::DiffLineIndex, syntax_highlight}, }; pub(crate) struct PullRequestDiffView { @@ -42,6 +42,7 @@ struct DiffSearchResult { cursor: DiffSearchResultCursor, old_side: Vec, new_side: Vec, + prev_diff_line_highlights: (usize, Vec), } struct DiffSearchResultCursor { @@ -308,6 +309,7 @@ impl PullRequestDiffView { cursor, old_side: old_search_result, new_side: new_search_result, + prev_diff_line_highlights: (0, Vec::new()), }); } @@ -387,23 +389,31 @@ impl PullRequestDiffView { if next_highlight_line == diff_line_i && 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 - // go to old side first + // next search hit candidates found on next diff line on both old side and new side + // 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.index += 1; + let (range, style) = &next.unwrap().highlight_range; + Some(( + range.clone(), + gpui::HighlightStyle { + background_color: Some(gpui::red()), + ..style.clone() + }, + )) } else { // next cursor should be on other side with the found index search_result.cursor.side = search_result.cursor.side.flipped(); search_result.cursor.diff_line_i = diff_line_i; search_result.cursor.index = other_side_i; + Some(( + r.clone(), + gpui::HighlightStyle { + background_color: Some(gpui::red()), + ..symbol_highlight_style(theme) + }, + )) } - Some(( - r.clone(), - gpui::HighlightStyle { - background_color: Some(gpui::red()), - ..symbol_highlight_style(theme) - }, - )) } else if let Some(next) = next { // stay on old side, point to next old side highlight 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::New => diff_view_state.new_side_highlights_mut(), }; - if let Some(mut content) = prev_highlighted_content { - content.replace_highlight_range(¤t_search_hit.highlight_range); + if let Some(mut content) = prev_highlighted_content + && !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::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); } } diff --git a/src/util/diff.rs b/src/util/diff.rs index 9877e16..7a4a623 100644 --- a/src/util/diff.rs +++ b/src/util/diff.rs @@ -1,4 +1,4 @@ -use std::{ops::Deref, sync::Arc}; +use std::sync::Arc; use similar::DiffableStr; diff --git a/src/util/mod.rs b/src/util/mod.rs index 91dd000..8300f37 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod diff; pub(crate) mod file; +pub(crate) mod range; pub(crate) mod str; pub(crate) mod syntax_highlight; pub(crate) mod timeout; diff --git a/src/util/range.rs b/src/util/range.rs new file mode 100644 index 0000000..f171a6c --- /dev/null +++ b/src/util/range.rs @@ -0,0 +1,17 @@ +use std::ops::Sub; + +pub(crate) trait RangeExt { + fn relative_to(&self, other: &std::ops::Range) -> std::ops::Range + where + T: Sub + Ord + PartialOrd + Clone + Copy; +} + +impl RangeExt for std::ops::Range +where + T: Sub + Ord + PartialOrd + Clone + Copy, +{ + fn relative_to(&self, other: &std::ops::Range) -> std::ops::Range { + debug_assert!(self.start >= other.start); + (self.start - other.start)..(self.end - other.start) + } +} diff --git a/src/util/syntax_highlight.rs b/src/util/syntax_highlight.rs index faa27c7..e67d6b9 100644 --- a/src/util/syntax_highlight.rs +++ b/src/util/syntax_highlight.rs @@ -1,6 +1,10 @@ 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, gpui::HighlightStyle); @@ -13,6 +17,68 @@ pub(crate) struct HighlightedLine { highlights: Vec<(std::ops::Range, 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, + b: impl IntoIterator, +) -> impl Iterator { + 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 = LazyLock::new(|| { [ 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 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(), new_highlights.drain(..).into_iter(), ) @@ -232,7 +298,7 @@ impl HighlightedContent { } 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(), new_highlights.drain(..).into_iter(), ) @@ -242,6 +308,7 @@ impl HighlightedContent { 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) -> Option { self.0 .binary_search_by(|line| { @@ -260,25 +327,79 @@ impl HighlightedContent { .ok() } - pub(crate) fn replace_highlight_range(&mut self, highlighted_range: &HighlightedRange) { - let (range, _) = &highlighted_range; + pub(crate) fn add_highlight_range( + &mut self, + highlighted_range: HighlightedRange, + ) -> Option { + 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_index_of_range(range) - .map(|line_i| &mut self.0[line_i]); + line.highlights = combine_highlights( + line.highlights.drain(..).into_iter(), + std::iter::once(( + highlighted_range.0.relative_to(&line.line_range), + highlighted_range.1, + )), + ) + .collect(); - 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)) - }); + Some(line_i) + } - 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, + ) { + 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::>(); + + 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::>(); + + assert_eq!( + combined, + [ + (0..1, background(gpui::yellow())), + (1..3, background(gpui::red())), + (3..5, background(gpui::red())), + (5..6, background(gpui::green())), + ] + ); } }