gpui_component/searchable_list/
vec.rs1use gpui::{App, SharedString, Task, Window};
2
3use crate::IndexPath;
4
5use super::delegate::{SearchableListDelegate, SearchableListItem};
6
7impl SearchableListItem for String {
10 type Value = Self;
11
12 fn title(&self) -> SharedString {
13 SharedString::from(self.clone())
14 }
15
16 fn value(&self) -> &Self::Value {
17 self
18 }
19}
20
21impl SearchableListItem for SharedString {
22 type Value = Self;
23
24 fn title(&self) -> SharedString {
25 self.clone()
26 }
27
28 fn value(&self) -> &Self::Value {
29 self
30 }
31}
32
33impl SearchableListItem for &'static str {
34 type Value = Self;
35
36 fn title(&self) -> SharedString {
37 SharedString::from(*self)
38 }
39
40 fn value(&self) -> &Self::Value {
41 self
42 }
43}
44
45impl<T: SearchableListItem + 'static> SearchableListDelegate for Vec<T> {
48 type Item = T;
49
50 fn items_count(&self, _: usize) -> usize {
51 self.len()
52 }
53
54 fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
55 self.as_slice().get(ix.row)
56 }
57
58 fn position<V>(&self, value: &V) -> Option<IndexPath>
59 where
60 Self::Item: SearchableListItem<Value = V>,
61 V: PartialEq,
62 {
63 self.iter()
64 .position(|v| v.value() == value)
65 .map(|ix| IndexPath::default().row(ix))
66 }
67}
68
69#[derive(Debug, Clone)]
76pub struct SearchableVec<T> {
77 items: Vec<T>,
78 matched: Vec<Matched>,
79}
80
81#[derive(Debug, Clone)]
83struct Matched {
84 ix: usize,
85 rows: Option<Vec<usize>>,
87}
88
89impl Matched {
90 fn all(ix: usize) -> Self {
91 Self { ix, rows: None }
92 }
93
94 fn len(&self, all: usize) -> usize {
96 self.rows.as_ref().map_or(all, Vec::len)
97 }
98
99 fn row(&self, row: usize) -> Option<usize> {
100 match &self.rows {
101 Some(rows) => rows.get(row).copied(),
102 None => Some(row),
103 }
104 }
105}
106
107impl<T> SearchableVec<T> {
108 fn from_items(items: Vec<T>) -> Self {
109 Self {
110 matched: (0..items.len()).map(Matched::all).collect(),
111 items,
112 }
113 }
114
115 fn matched_item(&self, ix: usize) -> Option<(&Matched, &T)> {
116 let matched = self.matched.get(ix)?;
117 Some((matched, self.items.get(matched.ix)?))
118 }
119
120 fn matched_items(&self) -> impl Iterator<Item = (&Matched, &T)> {
121 self.matched
122 .iter()
123 .filter_map(|matched| Some((matched, self.items.get(matched.ix)?)))
124 }
125}
126
127impl<T: Clone> SearchableVec<T> {
128 pub fn new(items: impl Into<Vec<T>>) -> Self {
130 Self::from_items(items.into())
131 }
132
133 pub fn push(&mut self, item: T) {
135 self.matched.push(Matched::all(self.items.len()));
136 self.items.push(item);
137 }
138}
139
140impl<T: SearchableListItem> From<Vec<T>> for SearchableVec<T> {
141 fn from(items: Vec<T>) -> Self {
142 Self::from_items(items)
143 }
144}
145
146impl<I: SearchableListItem + 'static> SearchableListDelegate for SearchableVec<I> {
147 type Item = I;
148
149 fn items_count(&self, _: usize) -> usize {
150 self.matched.len()
151 }
152
153 fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
154 self.matched_item(ix.row).map(|(_, item)| item)
155 }
156
157 fn position<V>(&self, value: &V) -> Option<IndexPath>
158 where
159 Self::Item: SearchableListItem<Value = V>,
160 V: PartialEq,
161 {
162 self.matched_items()
163 .position(|(_, v)| v.value() == value)
164 .map(|ix| IndexPath::default().row(ix))
165 }
166
167 fn perform_search(&mut self, query: &str, _: &mut Window, _: &mut App) -> Task<()> {
168 self.matched = self
169 .items
170 .iter()
171 .enumerate()
172 .filter(|(_, item)| item.matches(query))
173 .map(|(ix, _)| Matched::all(ix))
174 .collect();
175
176 Task::ready(())
177 }
178}
179
180#[derive(Debug, Clone)]
184pub struct SearchableGroup<I: SearchableListItem> {
185 pub title: SharedString,
186 pub items: Vec<I>,
187}
188
189impl<I: SearchableListItem> SearchableGroup<I> {
190 pub fn new(title: impl Into<SharedString>) -> Self {
192 Self {
193 title: title.into(),
194 items: vec![],
195 }
196 }
197
198 pub fn item(mut self, item: I) -> Self {
200 self.items.push(item);
201 self
202 }
203
204 pub fn items(mut self, items: impl IntoIterator<Item = I>) -> Self {
206 self.items.extend(items);
207 self
208 }
209
210 fn matched_rows(&self, query: &str) -> Option<Vec<usize>> {
213 let rows: Vec<usize> = self
214 .items
215 .iter()
216 .enumerate()
217 .filter(|(_, item)| item.matches(query))
218 .map(|(ix, _)| ix)
219 .collect();
220
221 (!rows.is_empty() || self.title.to_lowercase().contains(&query.to_lowercase()))
222 .then_some(rows)
223 }
224}
225
226impl<I: SearchableListItem + 'static> SearchableListDelegate for SearchableVec<SearchableGroup<I>> {
227 type Item = I;
228
229 fn sections_count(&self, _: &App) -> usize {
230 self.matched.len()
231 }
232
233 fn items_count(&self, section: usize) -> usize {
234 self.matched_item(section)
235 .map_or(0, |(matched, group)| matched.len(group.items.len()))
236 }
237
238 fn section(&self, section: usize) -> Option<gpui::AnyElement> {
239 use gpui::IntoElement as _;
240
241 let (_, group) = self.matched_item(section)?;
242 Some(group.title.clone().into_any_element())
243 }
244
245 fn item(&self, ix: IndexPath) -> Option<&Self::Item> {
246 let (matched, group) = self.matched_item(ix.section)?;
247
248 group.items.get(matched.row(ix.row)?)
249 }
250
251 fn position<V>(&self, value: &V) -> Option<IndexPath>
252 where
253 Self::Item: SearchableListItem<Value = V>,
254 V: PartialEq,
255 {
256 for (ix, (matched, group)) in self.matched_items().enumerate() {
257 for row_ix in 0..matched.len(group.items.len()) {
258 let item = matched.row(row_ix).and_then(|row| group.items.get(row));
259 if item.is_some_and(|item| item.value() == value) {
260 return Some(IndexPath::default().section(ix).row(row_ix));
261 }
262 }
263 }
264
265 None
266 }
267
268 fn perform_search(&mut self, query: &str, _: &mut Window, _: &mut App) -> Task<()> {
269 self.matched = self
270 .items
271 .iter()
272 .enumerate()
273 .filter_map(|(ix, group)| {
274 let rows = group.matched_rows(query)?;
275 Some(Matched {
276 ix,
277 rows: Some(rows),
278 })
279 })
280 .collect();
281
282 Task::ready(())
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use gpui::TestAppContext;
289
290 use super::*;
291
292 #[gpui::test]
293 fn test_searchable_vec_maps_matched_rows_to_items(cx: &mut TestAppContext) {
294 let cx = cx.add_empty_window();
295 cx.update(|window, cx| {
296 let mut items = SearchableVec::new(vec!["Rust", "Go", "Ruby"]);
297 _ = items.perform_search("ru", window, cx);
298 assert_eq!(items.items_count(0), 2);
299 assert_eq!(items.item(IndexPath::new(1)), Some(&"Ruby"));
300 assert_eq!(items.position(&"Ruby"), Some(IndexPath::new(1)));
301 assert_eq!(items.position(&"Go"), None);
302
303 items.push("Rune");
304 assert_eq!(items.item(IndexPath::new(2)), Some(&"Rune"));
305
306 _ = items.perform_search("", window, cx);
307 assert_eq!(items.items_count(0), 4);
308 });
309 }
310
311 #[gpui::test]
312 fn test_searchable_group_keeps_matched_rows(cx: &mut TestAppContext) {
313 let cx = cx.add_empty_window();
314 cx.update(|window, cx| {
315 let mut groups = SearchableVec::new(vec![
316 SearchableGroup::new("Fruits").items(["Apple", "Banana"]),
317 SearchableGroup::new("Berries").items(["Blueberry", "Cranberry"]),
318 SearchableGroup::new("Nuts").items(["Almond"]),
319 ]);
320
321 _ = groups.perform_search("cran", window, cx);
322 assert_eq!(groups.sections_count(cx), 1);
323 assert_eq!(groups.items_count(0), 1);
324 assert_eq!(groups.item(IndexPath::new(0)), Some(&"Cranberry"));
325 assert_eq!(groups.position(&"Cranberry"), Some(IndexPath::new(0)));
326 assert_eq!(groups.position(&"Blueberry"), None);
327
328 _ = groups.perform_search("nuts", window, cx);
330 assert_eq!(groups.sections_count(cx), 1);
331 assert_eq!(groups.items_count(0), 0);
332
333 _ = groups.perform_search("b", window, cx);
334 assert_eq!(groups.sections_count(cx), 2);
335 assert_eq!(groups.items_count(1), 2);
336 assert_eq!(
337 groups.position(&"Cranberry"),
338 Some(IndexPath::new(1).section(1))
339 );
340 });
341 }
342}