diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 225ccd97e4..5754f8be2a 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1276,11 +1276,16 @@ impl CompletionsMenu { &None }; - let highlights = combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - &style.text, - styled_runs_for_code_label(&completion.label, &style.syntax), - &mat.positions, + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), + styled_runs_for_code_label(&completion.label, &style.syntax).map( + |(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + (range, highlight) + }, + ), ); let completion_label = StyledText::new(completion.label.text.clone()) .with_runs(text_runs_for_highlights( @@ -10056,75 +10061,6 @@ pub fn text_runs_for_highlights( runs } -pub fn combine_syntax_and_fuzzy_match_highlights( - text: &str, - default_style: &TextStyle, - syntax_ranges: impl Iterator, HighlightStyle)>, - match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut highlights = Vec::new(); - let mut match_indices = match_indices.iter().copied().peekable(); - - for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) - { - syntax_highlight.font_weight = None; - - // Add highlights for any fuzzy match characters before the next - // syntax highlight range. - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.start { - break; - } - match_indices.next(); - let end_index = char_ix_after(match_index, text); - highlights.push((match_index..end_index, FontWeight::BOLD.into())); - } - - if range.start == usize::MAX { - break; - } - - // Add highlights for any fuzzy match characters within the - // syntax highlight range. - let mut offset = range.start; - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.end { - break; - } - - match_indices.next(); - if match_index > offset { - highlights.push((offset..match_index, syntax_highlight)); - } - - let mut end_index = char_ix_after(match_index, text); - while let Some(&next_match_index) = match_indices.peek() { - if next_match_index == end_index && next_match_index < range.end { - end_index = char_ix_after(next_match_index, text); - match_indices.next(); - } else { - break; - } - } - - let mut match_style = syntax_highlight; - match_style.font_weight = Some(FontWeight::BOLD); - highlights.push((match_index..end_index, match_style)); - offset = end_index; - } - - if offset < range.end { - highlights.push((offset..range.end, syntax_highlight)); - } - } - - fn char_ix_after(ix: usize, text: &str) -> usize { - ix + text[ix..].chars().next().unwrap().len_utf8() - } - - highlights -} - pub fn styled_runs_for_code_label<'a>( label: &'a CodeLabel, syntax_theme: &'a theme::SyntaxTheme, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 3a0de328e5..6865e81cfa 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { // ); // } -#[test] -fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Some(Hsla::red()), - ..Default::default() - }, - ), - ( - 4..8, - HighlightStyle { - color: Some(Hsla::green()), - ..Default::default() - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - string, - &TextStyle::default(), - syntax_ranges.into_iter(), - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Some(Hsla::red()), - ..Default::default() - }, - ), - ( - 4..5, - HighlightStyle { - color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - 5..6, - HighlightStyle { - color: Some(Hsla::green()), - ..Default::default() - }, - ), - ( - 6..8, - HighlightStyle { - color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - 8..9, - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ] - ); -} - #[gpui::test] async fn go_to_prev_overlapping_diagnostic( executor: BackgroundExecutor, diff --git a/crates/fuzzy2/src/strings.rs b/crates/fuzzy2/src/strings.rs index 085362dd2c..ab3de3fa44 100644 --- a/crates/fuzzy2/src/strings.rs +++ b/crates/fuzzy2/src/strings.rs @@ -6,6 +6,8 @@ use gpui::BackgroundExecutor; use std::{ borrow::Cow, cmp::{self, Ordering}, + iter, + ops::Range, sync::atomic::AtomicBool, }; @@ -54,6 +56,30 @@ pub struct StringMatch { pub string: String, } +impl StringMatch { + pub fn ranges<'a>(&'a self) -> impl 'a + Iterator> { + let mut positions = self.positions.iter().peekable(); + iter::from_fn(move || { + while let Some(start) = positions.next().copied() { + let mut end = start + self.char_len_at_index(start); + while let Some(next_start) = positions.peek() { + if end == **next_start { + end += self.char_len_at_index(end); + positions.next(); + } + } + + return Some(start..end); + } + None + }) + } + + fn char_len_at_index(&self, ix: usize) -> usize { + self.string[ix..].chars().next().unwrap().len_utf8() + } +} + impl PartialEq for StringMatch { fn eq(&self, other: &Self) -> bool { self.cmp(other).is_eq() diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 472b81c0f8..640538fff0 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,9 +1,12 @@ +use std::{iter, mem, ops::Range}; + use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, }; +use collections::HashSet; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; pub use taffy::style::{ @@ -512,6 +515,15 @@ impl From for HighlightStyle { } } +impl From for HighlightStyle { + fn from(font_style: FontStyle) -> Self { + Self { + font_style: Some(font_style), + ..Default::default() + } + } +} + impl From for HighlightStyle { fn from(color: Rgba) -> Self { Self { @@ -520,3 +532,140 @@ impl From for HighlightStyle { } } } + +pub fn combine_highlights( + a: impl IntoIterator, HighlightStyle)>, + b: impl IntoIterator, HighlightStyle)>, +) -> impl Iterator, HighlightStyle)> { + 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 endpoints = endpoints.into_iter().peekable(); + + let mut active_styles = HashSet::default(); + let mut ix = 0; + iter::from_fn(move || { + while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { + let prev_index = mem::replace(&mut ix, *endpoint_ix); + if ix > prev_index && !active_styles.is_empty() { + let mut current_style = HighlightStyle::default(); + for highlight_id in &active_styles { + current_style.highlight(highlights[*highlight_id]); + } + return Some((prev_index..ix, current_style)); + } + + if *is_start { + active_styles.insert(*highlight_id); + } else { + active_styles.remove(highlight_id); + } + endpoints.next(); + } + None + }) +} + +#[cfg(test)] +mod tests { + use crate::{blue, green, red, yellow}; + + use super::*; + + #[test] + fn test_combine_highlights() { + assert_eq!( + combine_highlights( + [ + (0..5, green().into()), + (4..10, FontWeight::BOLD.into()), + (15..20, yellow().into()), + ], + [ + (2..6, FontStyle::Italic.into()), + (1..3, blue().into()), + (21..23, red().into()), + ] + ) + .collect::>(), + [ + ( + 0..1, + HighlightStyle { + color: Some(green()), + ..Default::default() + } + ), + ( + 1..2, + HighlightStyle { + color: Some(blue()), + ..Default::default() + } + ), + ( + 2..3, + HighlightStyle { + color: Some(blue()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 3..4, + HighlightStyle { + color: Some(green()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 4..5, + HighlightStyle { + color: Some(green()), + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 5..6, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 6..10, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + } + ), + ( + 15..20, + HighlightStyle { + color: Some(yellow()), + ..Default::default() + } + ), + ( + 21..23, + HighlightStyle { + color: Some(red()), + ..Default::default() + } + ) + ] + ); + } +}