highlighted_match_with_paths.rs

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