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