1use std::ops::Range;
2
3use gpui::{
4 div, prelude::FluentBuilder, rems, App, HighlightStyle, IntoElement, ParentElement, RenderOnce,
5 SharedString, StyleRefinement, Styled, StyledText, Window,
6};
7
8use crate::{ActiveTheme, StyledExt};
9
10const MASKED: &'static str = "•";
11
12#[derive(Clone)]
14pub enum HighlightsMatch {
15 Prefix(SharedString),
16 Full(SharedString),
17}
18
19impl HighlightsMatch {
20 pub fn as_str(&self) -> &str {
21 match self {
22 Self::Prefix(s) => s.as_str(),
23 Self::Full(s) => s.as_str(),
24 }
25 }
26
27 #[inline]
28 pub fn is_prefix(&self) -> bool {
29 matches!(self, Self::Prefix(_))
30 }
31}
32
33impl From<&str> for HighlightsMatch {
34 fn from(value: &str) -> Self {
35 Self::Full(value.to_string().into())
36 }
37}
38
39impl From<String> for HighlightsMatch {
40 fn from(value: String) -> Self {
41 Self::Full(value.into())
42 }
43}
44
45impl From<SharedString> for HighlightsMatch {
46 fn from(value: SharedString) -> Self {
47 Self::Full(value)
48 }
49}
50
51#[derive(IntoElement)]
53pub struct Label {
54 style: StyleRefinement,
55 label: SharedString,
56 secondary: Option<SharedString>,
57 masked: bool,
58 highlights_text: Option<HighlightsMatch>,
59}
60
61impl Label {
62 pub fn new(label: impl Into<SharedString>) -> Self {
64 let label: SharedString = label.into();
65 Self {
66 style: Default::default(),
67 label,
68 secondary: None,
69 masked: false,
70 highlights_text: None,
71 }
72 }
73
74 pub fn secondary(mut self, secondary: impl Into<SharedString>) -> Self {
77 self.secondary = Some(secondary.into());
78 self
79 }
80
81 pub fn masked(mut self, masked: bool) -> Self {
83 self.masked = masked;
84 self
85 }
86
87 pub fn highlights(mut self, text: impl Into<HighlightsMatch>) -> Self {
89 self.highlights_text = Some(text.into());
90 self
91 }
92
93 fn full_text(&self) -> SharedString {
94 match &self.secondary {
95 Some(secondary) => format!("{} {}", self.label, secondary).into(),
96 None => self.label.clone(),
97 }
98 }
99
100 fn highlight_ranges(&self, total_length: usize) -> Vec<Range<usize>> {
101 let mut ranges = Vec::new();
102 let full_text = self.full_text();
103
104 if self.secondary.is_some() {
105 ranges.push(0..self.label.len());
106 ranges.push(self.label.len()..total_length);
107 }
108
109 if let Some(matched) = &self.highlights_text {
110 let matched_str = matched.as_str();
111 if !matched_str.is_empty() {
112 let search_lower = matched_str.to_lowercase();
113 let full_text_lower = full_text.to_lowercase();
114
115 if matched.is_prefix() {
116 if full_text_lower.starts_with(&search_lower) {
118 ranges.push(0..matched_str.len());
119 }
120 } else {
121 let mut search_start = 0;
123 while let Some(pos) = full_text_lower[search_start..].find(&search_lower) {
124 let match_start = search_start + pos;
125 let match_end = match_start + matched_str.len();
126
127 if match_end <= full_text.len() {
128 ranges.push(match_start..match_end);
129 }
130
131 search_start = match_start + 1;
132 while !full_text.is_char_boundary(search_start)
133 && search_start < full_text.len()
134 {
135 search_start += 1;
136 }
137
138 if search_start >= full_text.len() {
139 break;
140 }
141 }
142 }
143 }
144 }
145
146 ranges
147 }
148
149 fn measure_highlights(
150 &self,
151 length: usize,
152 cx: &mut App,
153 ) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
154 let ranges = self.highlight_ranges(length);
155 if ranges.is_empty() {
156 return None;
157 }
158
159 let mut highlights = Vec::new();
160 let mut highlight_ranges_added = 0;
161
162 if self.secondary.is_some() {
163 highlights.push((ranges[0].clone(), HighlightStyle::default()));
164 highlights.push((
165 ranges[1].clone(),
166 HighlightStyle {
167 color: Some(cx.theme().muted_foreground),
168 ..Default::default()
169 },
170 ));
171 highlight_ranges_added = 2;
172 }
173
174 for range in ranges.iter().skip(highlight_ranges_added) {
175 highlights.push((
176 range.clone(),
177 HighlightStyle {
178 color: Some(cx.theme().blue),
179 ..Default::default()
180 },
181 ));
182 }
183
184 Some(gpui::combine_highlights(vec![], highlights).collect())
185 }
186}
187
188impl Styled for Label {
189 fn style(&mut self) -> &mut gpui::StyleRefinement {
190 &mut self.style
191 }
192}
193
194impl RenderOnce for Label {
195 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
196 let mut text = self.full_text();
197 let chars_count = text.chars().count();
198
199 if self.masked {
200 text = SharedString::from(MASKED.repeat(chars_count))
201 };
202
203 let highlights = self.measure_highlights(text.len(), cx);
204
205 div()
206 .line_height(rems(1.25))
207 .text_color(cx.theme().foreground)
208 .refine_style(&self.style)
209 .child(
210 StyledText::new(&text).when_some(highlights, |this, hl| this.with_highlights(hl)),
211 )
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_highlight_ranges() {
221 let label = Label::new("Hello World");
225 let result = label.highlight_ranges("Hello World".len());
226 assert_eq!(result, Vec::<Range<usize>>::new());
227
228 let label = Label::new("Hello").secondary("World");
230 let total_length = "Hello World".len();
231 let result = label.highlight_ranges(total_length);
232 assert_eq!(result.len(), 2);
233 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); let label = Label::new("Hello World").highlights("WORLD");
240 let result = label.highlight_ranges("Hello World".len());
241 assert_eq!(result.len(), 1);
242 assert_eq!(result[0], 6..11); let label = Label::new("Hello Hello Hello").highlights("Hello");
246 let result = label.highlight_ranges("Hello Hello Hello".len());
247 assert_eq!(result.len(), 3);
248 assert_eq!(result[0], 0..5); assert_eq!(result[1], 6..11); assert_eq!(result[2], 12..17); let label = Label::new("Hello World").highlights("xyz");
254 let result = label.highlight_ranges("Hello World".len());
255 assert_eq!(result, Vec::<Range<usize>>::new());
256
257 let label = Label::new("Hello World").highlights("");
258 let result = label.highlight_ranges("Hello World".len());
259 assert_eq!(result, Vec::<Range<usize>>::new());
260
261 let label = Label::new("Hello").secondary("World").highlights("llo");
265 let total_length = "Hello World".len();
266 let result = label.highlight_ranges(total_length);
267 assert_eq!(result.len(), 3);
268 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 2..5); let label = Label::new("Hello").secondary("World").highlights("World");
274 let total_length = "Hello World".len();
275 let result = label.highlight_ranges(total_length);
276 assert_eq!(result.len(), 3);
277 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 6..11); let label = Label::new("Hello").secondary("World").highlights("o W");
283 let total_length = "Hello World".len();
284 let result = label.highlight_ranges(total_length);
285 assert_eq!(result.len(), 3);
286 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); assert_eq!(result[2], 4..7); let label = Label::new("aaaa").highlights("aa");
294 let result = label.highlight_ranges("aaaa".len());
295 assert!(result.len() >= 2);
296 assert_eq!(result[0], 0..2); assert_eq!(result[1], 1..3); let label = Label::new("你好世界,Hello World").highlights("世界");
301 let result = label.highlight_ranges("你好世界,Hello World".len());
302 assert_eq!(result.len(), 1);
303 let text = "你好世界,Hello World";
304 let start = text.find("世界").unwrap();
305 let end = start + "世界".len();
306 assert_eq!(result[0], start..end);
307 }
308
309 #[test]
310 fn test_highlight_ranges_prefix() {
311 let label = Label::new("aaaa").highlights(HighlightsMatch::Prefix("aa".into()));
313 let result = label.highlight_ranges("aaaa".len());
314 assert_eq!(result.len(), 1);
315 assert_eq!(result[0], 0..2); let label_full =
319 Label::new("Hello Hello").highlights(HighlightsMatch::Full("Hello".into()));
320 let result_full = label_full.highlight_ranges("Hello Hello".len());
321 assert_eq!(result_full.len(), 2); let label_prefix =
324 Label::new("Hello Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
325 let result_prefix = label_prefix.highlight_ranges("Hello Hello".len());
326 assert_eq!(result_prefix.len(), 1); assert_eq!(result_prefix[0], 0..5);
328
329 let label =
331 Label::new("Hello hello HELLO").highlights(HighlightsMatch::Prefix("hello".into()));
332 let result = label.highlight_ranges("Hello hello HELLO".len());
333 assert_eq!(result.len(), 1);
334 assert_eq!(result[0], 0..5); let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("xyz".into()));
338 let result = label.highlight_ranges("Hello World".len());
339 assert_eq!(result.len(), 0);
340
341 let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("".into()));
343 let result = label.highlight_ranges("Hello World".len());
344 assert_eq!(result.len(), 0);
345
346 let label = Label::new("Hello")
348 .secondary("Hello World")
349 .highlights(HighlightsMatch::Prefix("Hello".into()));
350 let total_length = "Hello Hello World".len();
351 let result = label.highlight_ranges(total_length);
352 assert_eq!(result.len(), 3); assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..17); assert_eq!(result[2], 0..5); let label = Label::new("abc")
359 .secondary("def abc def")
360 .highlights(HighlightsMatch::Prefix("abc".into()));
361 let total_length = "abc def abc def".len();
362 let result = label.highlight_ranges(total_length);
363 assert_eq!(result.len(), 3); assert_eq!(result[0], 0..3); assert_eq!(result[1], 3..15); assert_eq!(result[2], 0..3); let label = Label::new("你好世界你好").highlights(HighlightsMatch::Prefix("你好".into()));
370 let result = label.highlight_ranges("你好世界你好".len());
371 assert_eq!(result.len(), 1);
372 assert_eq!(result[0], 0..6); let label = Label::new("abababab").highlights(HighlightsMatch::Prefix("abab".into()));
376 let result = label.highlight_ranges("abababab".len());
377 assert_eq!(result.len(), 1);
378 assert_eq!(result[0], 0..4); let label =
382 Label::new("xyz Hello abc Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
383 let result = label.highlight_ranges("xyz Hello abc Hello".len());
384 assert_eq!(result.len(), 0); let prefix_match = HighlightsMatch::Prefix("test".into());
388 let full_match = HighlightsMatch::Full("test".into());
389 assert!(prefix_match.is_prefix());
390 assert!(!full_match.is_prefix());
391
392 let prefix_match = HighlightsMatch::Prefix("test".into());
394 assert_eq!(prefix_match.as_str(), "test");
395 }
396}