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)]
13pub enum HighlightsMatch {
14 Prefix(SharedString),
15 Full(SharedString),
16}
17
18impl HighlightsMatch {
19 pub fn as_str(&self) -> &str {
20 match self {
21 Self::Prefix(s) => s.as_str(),
22 Self::Full(s) => s.as_str(),
23 }
24 }
25
26 #[inline]
27 pub fn is_prefix(&self) -> bool {
28 matches!(self, Self::Prefix(_))
29 }
30}
31
32impl From<&str> for HighlightsMatch {
33 fn from(value: &str) -> Self {
34 Self::Full(value.to_string().into())
35 }
36}
37
38impl From<String> for HighlightsMatch {
39 fn from(value: String) -> Self {
40 Self::Full(value.into())
41 }
42}
43
44impl From<SharedString> for HighlightsMatch {
45 fn from(value: SharedString) -> Self {
46 Self::Full(value)
47 }
48}
49
50#[derive(IntoElement)]
51pub struct Label {
52 style: StyleRefinement,
53 label: SharedString,
54 secondary: Option<SharedString>,
55 masked: bool,
56 highlights_text: Option<HighlightsMatch>,
57}
58
59impl Label {
60 pub fn new(label: impl Into<SharedString>) -> Self {
61 let label: SharedString = label.into();
62 Self {
63 style: Default::default(),
64 label,
65 secondary: None,
66 masked: false,
67 highlights_text: None,
68 }
69 }
70
71 pub fn secondary(mut self, secondary: impl Into<SharedString>) -> Self {
74 self.secondary = Some(secondary.into());
75 self
76 }
77
78 pub fn masked(mut self, masked: bool) -> Self {
79 self.masked = masked;
80 self
81 }
82
83 pub fn highlights(mut self, text: impl Into<HighlightsMatch>) -> Self {
84 self.highlights_text = Some(text.into());
85 self
86 }
87
88 fn full_text(&self) -> SharedString {
89 match &self.secondary {
90 Some(secondary) => format!("{} {}", self.label, secondary).into(),
91 None => self.label.clone(),
92 }
93 }
94
95 fn highlight_ranges(&self, total_length: usize) -> Vec<Range<usize>> {
96 let mut ranges = Vec::new();
97 let full_text = self.full_text();
98
99 if self.secondary.is_some() {
100 ranges.push(0..self.label.len());
101 ranges.push(self.label.len()..total_length);
102 }
103
104 if let Some(matched) = &self.highlights_text {
105 let matched_str = matched.as_str();
106 if !matched_str.is_empty() {
107 let search_lower = matched_str.to_lowercase();
108 let full_text_lower = full_text.to_lowercase();
109
110 if matched.is_prefix() {
111 if full_text_lower.starts_with(&search_lower) {
113 ranges.push(0..matched_str.len());
114 }
115 } else {
116 let mut search_start = 0;
118 while let Some(pos) = full_text_lower[search_start..].find(&search_lower) {
119 let match_start = search_start + pos;
120 let match_end = match_start + matched_str.len();
121
122 if match_end <= full_text.len() {
123 ranges.push(match_start..match_end);
124 }
125
126 search_start = match_start + 1;
127 while !full_text.is_char_boundary(search_start)
128 && search_start < full_text.len()
129 {
130 search_start += 1;
131 }
132
133 if search_start >= full_text.len() {
134 break;
135 }
136 }
137 }
138 }
139 }
140
141 ranges
142 }
143
144 fn measure_highlights(
145 &self,
146 length: usize,
147 cx: &mut App,
148 ) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
149 let ranges = self.highlight_ranges(length);
150 if ranges.is_empty() {
151 return None;
152 }
153
154 let mut highlights = Vec::new();
155 let mut highlight_ranges_added = 0;
156
157 if self.secondary.is_some() {
158 highlights.push((ranges[0].clone(), HighlightStyle::default()));
159 highlights.push((
160 ranges[1].clone(),
161 HighlightStyle {
162 color: Some(cx.theme().muted_foreground),
163 ..Default::default()
164 },
165 ));
166 highlight_ranges_added = 2;
167 }
168
169 for range in ranges.iter().skip(highlight_ranges_added) {
170 highlights.push((
171 range.clone(),
172 HighlightStyle {
173 color: Some(cx.theme().blue),
174 ..Default::default()
175 },
176 ));
177 }
178
179 Some(highlights)
180 }
181}
182
183impl Styled for Label {
184 fn style(&mut self) -> &mut gpui::StyleRefinement {
185 &mut self.style
186 }
187}
188
189impl RenderOnce for Label {
190 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
191 let mut text = self.full_text();
192 let chars_count = text.chars().count();
193
194 if self.masked {
195 text = SharedString::from(MASKED.repeat(chars_count))
196 };
197
198 let highlights = self.measure_highlights(text.len(), cx);
199
200 div()
201 .line_height(rems(1.25))
202 .text_color(cx.theme().foreground)
203 .refine_style(&self.style)
204 .child(
205 StyledText::new(&text).when_some(highlights, |this, hl| this.with_highlights(hl)),
206 )
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_highlight_ranges() {
216 let label = Label::new("Hello World");
220 let result = label.highlight_ranges("Hello World".len());
221 assert_eq!(result, Vec::<Range<usize>>::new());
222
223 let label = Label::new("Hello").secondary("World");
225 let total_length = "Hello World".len();
226 let result = label.highlight_ranges(total_length);
227 assert_eq!(result.len(), 2);
228 assert_eq!(result[0], 0..5); assert_eq!(result[1], 5..11); let label = Label::new("Hello World").highlights("WORLD");
235 let result = label.highlight_ranges("Hello World".len());
236 assert_eq!(result.len(), 1);
237 assert_eq!(result[0], 6..11); let label = Label::new("Hello Hello Hello").highlights("Hello");
241 let result = label.highlight_ranges("Hello Hello Hello".len());
242 assert_eq!(result.len(), 3);
243 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");
249 let result = label.highlight_ranges("Hello World".len());
250 assert_eq!(result, Vec::<Range<usize>>::new());
251
252 let label = Label::new("Hello World").highlights("");
253 let result = label.highlight_ranges("Hello World".len());
254 assert_eq!(result, Vec::<Range<usize>>::new());
255
256 let label = Label::new("Hello").secondary("World").highlights("llo");
260 let total_length = "Hello World".len();
261 let result = label.highlight_ranges(total_length);
262 assert_eq!(result.len(), 3);
263 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");
269 let total_length = "Hello World".len();
270 let result = label.highlight_ranges(total_length);
271 assert_eq!(result.len(), 3);
272 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");
278 let total_length = "Hello World".len();
279 let result = label.highlight_ranges(total_length);
280 assert_eq!(result.len(), 3);
281 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");
289 let result = label.highlight_ranges("aaaa".len());
290 assert!(result.len() >= 2);
291 assert_eq!(result[0], 0..2); assert_eq!(result[1], 1..3); let label = Label::new("你好世界,Hello World").highlights("世界");
296 let result = label.highlight_ranges("你好世界,Hello World".len());
297 assert_eq!(result.len(), 1);
298 let text = "你好世界,Hello World";
299 let start = text.find("世界").unwrap();
300 let end = start + "世界".len();
301 assert_eq!(result[0], start..end);
302 }
303
304 #[test]
305 fn test_highlight_ranges_prefix() {
306 let label = Label::new("aaaa").highlights(HighlightsMatch::Prefix("aa".into()));
308 let result = label.highlight_ranges("aaaa".len());
309 assert_eq!(result.len(), 1);
310 assert_eq!(result[0], 0..2); let label_full =
314 Label::new("Hello Hello").highlights(HighlightsMatch::Full("Hello".into()));
315 let result_full = label_full.highlight_ranges("Hello Hello".len());
316 assert_eq!(result_full.len(), 2); let label_prefix =
319 Label::new("Hello Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
320 let result_prefix = label_prefix.highlight_ranges("Hello Hello".len());
321 assert_eq!(result_prefix.len(), 1); assert_eq!(result_prefix[0], 0..5);
323
324 let label =
326 Label::new("Hello hello HELLO").highlights(HighlightsMatch::Prefix("hello".into()));
327 let result = label.highlight_ranges("Hello hello HELLO".len());
328 assert_eq!(result.len(), 1);
329 assert_eq!(result[0], 0..5); let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("xyz".into()));
333 let result = label.highlight_ranges("Hello World".len());
334 assert_eq!(result.len(), 0);
335
336 let label = Label::new("Hello World").highlights(HighlightsMatch::Prefix("".into()));
338 let result = label.highlight_ranges("Hello World".len());
339 assert_eq!(result.len(), 0);
340
341 let label = Label::new("Hello")
343 .secondary("Hello World")
344 .highlights(HighlightsMatch::Prefix("Hello".into()));
345 let total_length = "Hello Hello World".len();
346 let result = label.highlight_ranges(total_length);
347 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")
354 .secondary("def abc def")
355 .highlights(HighlightsMatch::Prefix("abc".into()));
356 let total_length = "abc def abc def".len();
357 let result = label.highlight_ranges(total_length);
358 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()));
365 let result = label.highlight_ranges("你好世界你好".len());
366 assert_eq!(result.len(), 1);
367 assert_eq!(result[0], 0..6); let label = Label::new("abababab").highlights(HighlightsMatch::Prefix("abab".into()));
371 let result = label.highlight_ranges("abababab".len());
372 assert_eq!(result.len(), 1);
373 assert_eq!(result[0], 0..4); let label =
377 Label::new("xyz Hello abc Hello").highlights(HighlightsMatch::Prefix("Hello".into()));
378 let result = label.highlight_ranges("xyz Hello abc Hello".len());
379 assert_eq!(result.len(), 0); let prefix_match = HighlightsMatch::Prefix("test".into());
383 let full_match = HighlightsMatch::Full("test".into());
384 assert!(prefix_match.is_prefix());
385 assert!(!full_match.is_prefix());
386
387 let prefix_match = HighlightsMatch::Prefix("test".into());
389 assert_eq!(prefix_match.as_str(), "test");
390 }
391}