Extract a `gpui::combine_highlights` function

Antonio Scandurra created

Change summary

crates/editor2/src/editor.rs       |  84 ++---------------
crates/editor2/src/editor_tests.rs |  69 --------------
crates/fuzzy2/src/strings.rs       |  26 +++++
crates/gpui2/src/style.rs          | 149 ++++++++++++++++++++++++++++++++
4 files changed, 185 insertions(+), 143 deletions(-)

Detailed changes

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<Item = (Range<usize>, HighlightStyle)>,
-    match_indices: &[usize],
-) -> Vec<(Range<usize>, 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,

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,

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<Item = Range<usize>> {
+        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()

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<FontWeight> for HighlightStyle {
     }
 }
 
+impl From<FontStyle> for HighlightStyle {
+    fn from(font_style: FontStyle) -> Self {
+        Self {
+            font_style: Some(font_style),
+            ..Default::default()
+        }
+    }
+}
+
 impl From<Rgba> for HighlightStyle {
     fn from(color: Rgba) -> Self {
         Self {
@@ -520,3 +532,140 @@ impl From<Rgba> for HighlightStyle {
         }
     }
 }
+
+pub fn combine_highlights(
+    a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+    b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+) -> impl Iterator<Item = (Range<usize>, 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::<Vec<_>>(),
+            [
+                (
+                    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()
+                    }
+                )
+            ]
+        );
+    }
+}