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}