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}