highlighted_match_with_paths.rs

  1use ui::{HighlightedLabel, prelude::*};
  2
  3#[derive(Clone)]
  4pub struct HighlightedMatchWithPaths {
  5    pub match_label: HighlightedMatch,
  6    pub paths: Vec<HighlightedMatch>,
  7}
  8
  9#[derive(Debug, Clone, IntoElement)]
 10pub struct HighlightedMatch {
 11    pub text: String,
 12    pub highlight_positions: Vec<usize>,
 13    pub color: Color,
 14}
 15
 16impl HighlightedMatch {
 17    pub fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
 18        // Track a running byte offset and insert separators between parts.
 19        let mut first = true;
 20        let mut byte_offset = 0;
 21        let mut text = String::new();
 22        let mut highlight_positions = Vec::new();
 23        for component in components {
 24            if !first {
 25                text.push_str(separator);
 26                byte_offset += separator.len();
 27            }
 28            first = false;
 29
 30            highlight_positions.extend(
 31                component
 32                    .highlight_positions
 33                    .iter()
 34                    .map(|position| position + byte_offset),
 35            );
 36            text.push_str(&component.text);
 37            byte_offset += component.text.len();
 38        }
 39
 40        Self {
 41            text,
 42            highlight_positions,
 43            color: Color::Default,
 44        }
 45    }
 46
 47    pub fn color(self, color: Color) -> Self {
 48        Self { color, ..self }
 49    }
 50}
 51impl RenderOnce for HighlightedMatch {
 52    fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement {
 53        HighlightedLabel::new(self.text, self.highlight_positions).color(self.color)
 54    }
 55}
 56
 57impl HighlightedMatchWithPaths {
 58    pub fn render_paths_children(&mut self, element: Div) -> Div {
 59        element.children(self.paths.clone().into_iter().map(|path| {
 60            HighlightedLabel::new(path.text, path.highlight_positions)
 61                .size(LabelSize::Small)
 62                .color(Color::Muted)
 63        }))
 64    }
 65}
 66
 67impl RenderOnce for HighlightedMatchWithPaths {
 68    fn render(mut self, _window: &mut Window, _: &mut App) -> impl IntoElement {
 69        v_flex()
 70            .child(self.match_label.clone())
 71            .when(!self.paths.is_empty(), |this| {
 72                self.render_paths_children(this)
 73            })
 74    }
 75}
 76
 77#[cfg(test)]
 78mod tests {
 79    use super::*;
 80
 81    #[test]
 82    fn join_offsets_positions_by_bytes_not_chars() {
 83        // "αβγ" is 3 Unicode scalar values, 6 bytes in UTF-8.
 84        let left_text = "αβγ".to_string();
 85        let right_text = "label".to_string();
 86        let left = HighlightedMatch {
 87            text: left_text,
 88            highlight_positions: vec![],
 89            color: Color::Default,
 90        };
 91        let right = HighlightedMatch {
 92            text: right_text,
 93            highlight_positions: vec![0, 1],
 94            color: Color::Default,
 95        };
 96        let joined = HighlightedMatch::join([left, right].into_iter(), "");
 97
 98        assert!(
 99            joined
100                .highlight_positions
101                .iter()
102                .all(|&p| joined.text.is_char_boundary(p)),
103            "join produced non-boundary positions {:?} for text {:?}",
104            joined.highlight_positions,
105            joined.text
106        );
107    }
108}