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}