1use anyhow::Result;
2use editor::Editor;
3use gpui::{
4 div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent,
5 EventEmitter, FocusHandle, FocusableView, Length, ListState, Render, Task,
6 UniformListScrollHandle, View, ViewContext, WindowContext,
7};
8use std::{sync::Arc, time::Duration};
9use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing};
10use workspace::ModalView;
11
12enum ElementContainer {
13 List(ListState),
14 UniformList(UniformListScrollHandle),
15}
16
17struct PendingUpdateMatches {
18 delegate_update_matches: Option<Task<()>>,
19 _task: Task<Result<()>>,
20}
21
22pub struct Picker<D: PickerDelegate> {
23 pub delegate: D,
24 element_container: ElementContainer,
25 editor: View<Editor>,
26 pending_update_matches: Option<PendingUpdateMatches>,
27 confirm_on_update: Option<bool>,
28 width: Option<Length>,
29 max_height: Option<Length>,
30
31 /// Whether the `Picker` is rendered as a self-contained modal.
32 ///
33 /// Set this to `false` when rendering the `Picker` as part of a larger modal.
34 is_modal: bool,
35}
36
37pub trait PickerDelegate: Sized + 'static {
38 type ListItem: IntoElement;
39 fn match_count(&self) -> usize;
40 fn selected_index(&self) -> usize;
41 fn separators_after_indices(&self) -> Vec<usize> {
42 Vec::new()
43 }
44 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
45
46 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str>;
47 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
48
49 // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background
50 // work for up to `duration` to try and get a result synchronously.
51 // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes
52 // mostly work when dismissing a palette.
53 fn finalize_update_matches(
54 &mut self,
55 _query: String,
56 _duration: Duration,
57 _cx: &mut ViewContext<Picker<Self>>,
58 ) -> bool {
59 false
60 }
61
62 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
63 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
64
65 fn render_match(
66 &self,
67 ix: usize,
68 selected: bool,
69 cx: &mut ViewContext<Picker<Self>>,
70 ) -> Option<Self::ListItem>;
71 fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
72 None
73 }
74 fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
75 None
76 }
77}
78
79impl<D: PickerDelegate> FocusableView for Picker<D> {
80 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
81 self.editor.focus_handle(cx)
82 }
83}
84
85fn create_editor(placeholder: Arc<str>, cx: &mut WindowContext<'_>) -> View<Editor> {
86 cx.new_view(|cx| {
87 let mut editor = Editor::single_line(cx);
88 editor.set_placeholder_text(placeholder, cx);
89 editor
90 })
91}
92
93impl<D: PickerDelegate> Picker<D> {
94 /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
95 /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
96 pub fn uniform_list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
97 Self::new(delegate, cx, true)
98 }
99
100 /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
101 /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
102 pub fn list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
103 Self::new(delegate, cx, false)
104 }
105
106 fn new(delegate: D, cx: &mut ViewContext<Self>, is_uniform: bool) -> Self {
107 let editor = create_editor(delegate.placeholder_text(cx), cx);
108 cx.subscribe(&editor, Self::on_input_editor_event).detach();
109 let mut this = Self {
110 delegate,
111 editor,
112 element_container: Self::create_element_container(is_uniform, cx),
113 pending_update_matches: None,
114 confirm_on_update: None,
115 width: None,
116 max_height: None,
117 is_modal: true,
118 };
119 this.update_matches("".to_string(), cx);
120 // give the delegate 4ms to renderthe first set of suggestions.
121 this.delegate
122 .finalize_update_matches("".to_string(), Duration::from_millis(4), cx);
123 this
124 }
125
126 fn create_element_container(is_uniform: bool, cx: &mut ViewContext<Self>) -> ElementContainer {
127 if is_uniform {
128 ElementContainer::UniformList(UniformListScrollHandle::new())
129 } else {
130 let view = cx.view().downgrade();
131 ElementContainer::List(ListState::new(
132 0,
133 gpui::ListAlignment::Top,
134 px(1000.),
135 move |ix, cx| {
136 view.upgrade()
137 .map(|view| {
138 view.update(cx, |this, cx| {
139 this.render_element(cx, ix).into_any_element()
140 })
141 })
142 .unwrap_or_else(|| div().into_any_element())
143 },
144 ))
145 }
146 }
147
148 pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
149 self.width = Some(width.into());
150 self
151 }
152
153 pub fn max_height(mut self, max_height: impl Into<gpui::Length>) -> Self {
154 self.max_height = Some(max_height.into());
155 self
156 }
157
158 pub fn modal(mut self, modal: bool) -> Self {
159 self.is_modal = modal;
160 self
161 }
162
163 pub fn focus(&self, cx: &mut WindowContext) {
164 self.editor.update(cx, |editor, cx| editor.focus(cx));
165 }
166
167 pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
168 let count = self.delegate.match_count();
169 if count > 0 {
170 let index = self.delegate.selected_index();
171 let ix = if index == count - 1 { 0 } else { index + 1 };
172 self.delegate.set_selected_index(ix, cx);
173 self.scroll_to_item_index(ix);
174 cx.notify();
175 }
176 }
177
178 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
179 let count = self.delegate.match_count();
180 if count > 0 {
181 let index = self.delegate.selected_index();
182 let ix = if index == 0 { count - 1 } else { index - 1 };
183 self.delegate.set_selected_index(ix, cx);
184 self.scroll_to_item_index(ix);
185 cx.notify();
186 }
187 }
188
189 fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
190 let count = self.delegate.match_count();
191 if count > 0 {
192 self.delegate.set_selected_index(0, cx);
193 self.scroll_to_item_index(0);
194 cx.notify();
195 }
196 }
197
198 fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
199 let count = self.delegate.match_count();
200 if count > 0 {
201 self.delegate.set_selected_index(count - 1, cx);
202 self.scroll_to_item_index(count - 1);
203 cx.notify();
204 }
205 }
206
207 pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
208 let count = self.delegate.match_count();
209 let index = self.delegate.selected_index();
210 let new_index = if index + 1 == count { 0 } else { index + 1 };
211 self.delegate.set_selected_index(new_index, cx);
212 self.scroll_to_item_index(new_index);
213 cx.notify();
214 }
215
216 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
217 self.delegate.dismissed(cx);
218 cx.emit(DismissEvent);
219 }
220
221 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
222 if self.pending_update_matches.is_some()
223 && !self
224 .delegate
225 .finalize_update_matches(self.query(cx), Duration::from_millis(16), cx)
226 {
227 self.confirm_on_update = Some(false)
228 } else {
229 self.pending_update_matches.take();
230 self.delegate.confirm(false, cx);
231 }
232 }
233
234 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
235 if self.pending_update_matches.is_some()
236 && !self
237 .delegate
238 .finalize_update_matches(self.query(cx), Duration::from_millis(16), cx)
239 {
240 self.confirm_on_update = Some(true)
241 } else {
242 self.delegate.confirm(true, cx);
243 }
244 }
245
246 fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
247 cx.stop_propagation();
248 cx.prevent_default();
249 self.delegate.set_selected_index(ix, cx);
250 self.delegate.confirm(secondary, cx);
251 }
252
253 fn on_input_editor_event(
254 &mut self,
255 _: View<Editor>,
256 event: &editor::EditorEvent,
257 cx: &mut ViewContext<Self>,
258 ) {
259 match event {
260 editor::EditorEvent::BufferEdited => {
261 let query = self.editor.read(cx).text(cx);
262 self.update_matches(query, cx);
263 }
264 editor::EditorEvent::Blurred => {
265 self.cancel(&menu::Cancel, cx);
266 }
267 _ => {}
268 }
269 }
270
271 pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
272 let query = self.editor.read(cx).text(cx);
273 self.update_matches(query, cx);
274 }
275
276 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
277 let delegate_pending_update_matches = self.delegate.update_matches(query, cx);
278
279 self.matches_updated(cx);
280 // This struct ensures that we can synchronously drop the task returned by the
281 // delegate's `update_matches` method and the task that the picker is spawning.
282 // If we simply capture the delegate's task into the picker's task, when the picker's
283 // task gets synchronously dropped, the delegate's task would keep running until
284 // the picker's task has a chance of being scheduled, because dropping a task happens
285 // asynchronously.
286 self.pending_update_matches = Some(PendingUpdateMatches {
287 delegate_update_matches: Some(delegate_pending_update_matches),
288 _task: cx.spawn(|this, mut cx| async move {
289 let delegate_pending_update_matches = this.update(&mut cx, |this, _| {
290 this.pending_update_matches
291 .as_mut()
292 .unwrap()
293 .delegate_update_matches
294 .take()
295 .unwrap()
296 })?;
297 delegate_pending_update_matches.await;
298 this.update(&mut cx, |this, cx| {
299 this.matches_updated(cx);
300 })
301 }),
302 });
303 }
304
305 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
306 if let ElementContainer::List(state) = &mut self.element_container {
307 state.reset(self.delegate.match_count());
308 }
309
310 let index = self.delegate.selected_index();
311 self.scroll_to_item_index(index);
312 self.pending_update_matches = None;
313 if let Some(secondary) = self.confirm_on_update.take() {
314 self.delegate.confirm(secondary, cx);
315 }
316 cx.notify();
317 }
318
319 pub fn query(&self, cx: &AppContext) -> String {
320 self.editor.read(cx).text(cx)
321 }
322
323 pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
324 self.editor
325 .update(cx, |editor, cx| editor.set_text(query, cx));
326 }
327
328 fn scroll_to_item_index(&mut self, ix: usize) {
329 match &mut self.element_container {
330 ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
331 ElementContainer::UniformList(scroll_handle) => scroll_handle.scroll_to_item(ix),
332 }
333 }
334
335 fn render_element(&self, cx: &mut ViewContext<Self>, ix: usize) -> impl IntoElement {
336 div()
337 .id(("item", ix))
338 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
339 this.handle_click(ix, event.down.modifiers.command, cx)
340 }))
341 .children(
342 self.delegate
343 .render_match(ix, ix == self.delegate.selected_index(), cx),
344 )
345 .when(
346 self.delegate.separators_after_indices().contains(&ix),
347 |picker| {
348 picker
349 .border_color(cx.theme().colors().border_variant)
350 .border_b_1()
351 .pb(px(-1.0))
352 },
353 )
354 }
355
356 fn render_element_container(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
357 match &self.element_container {
358 ElementContainer::UniformList(scroll_handle) => uniform_list(
359 cx.view().clone(),
360 "candidates",
361 self.delegate.match_count(),
362 move |picker, visible_range, cx| {
363 visible_range
364 .map(|ix| picker.render_element(cx, ix))
365 .collect()
366 },
367 )
368 .py_2()
369 .track_scroll(scroll_handle.clone())
370 .into_any_element(),
371 ElementContainer::List(state) => list(state.clone())
372 .with_sizing_behavior(gpui::ListSizingBehavior::Infer)
373 .py_2()
374 .into_any_element(),
375 }
376 }
377}
378
379impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
380impl<D: PickerDelegate> ModalView for Picker<D> {}
381
382impl<D: PickerDelegate> Render for Picker<D> {
383 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
384 let picker_editor = h_flex()
385 .overflow_hidden()
386 .flex_none()
387 .h_9()
388 .px_4()
389 .child(self.editor.clone());
390
391 div()
392 .key_context("Picker")
393 .size_full()
394 .when_some(self.width, |el, width| el.w(width))
395 .overflow_hidden()
396 // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
397 // as a part of a modal rather than the entire modal.
398 //
399 // We should revisit how the `Picker` is styled to make it more composable.
400 .when(self.is_modal, |this| this.elevation_3(cx))
401 .on_action(cx.listener(Self::select_next))
402 .on_action(cx.listener(Self::select_prev))
403 .on_action(cx.listener(Self::select_first))
404 .on_action(cx.listener(Self::select_last))
405 .on_action(cx.listener(Self::cancel))
406 .on_action(cx.listener(Self::confirm))
407 .on_action(cx.listener(Self::secondary_confirm))
408 .child(picker_editor)
409 .child(Divider::horizontal())
410 .when(self.delegate.match_count() > 0, |el| {
411 el.child(
412 v_flex()
413 .flex_grow()
414 .max_h(self.max_height.unwrap_or(rems(18.).into()))
415 .overflow_hidden()
416 .children(self.delegate.render_header(cx))
417 .child(self.render_element_container(cx)),
418 )
419 })
420 .when(self.delegate.match_count() == 0, |el| {
421 el.child(
422 v_flex().flex_grow().py_2().child(
423 ListItem::new("empty_state")
424 .inset(true)
425 .spacing(ListItemSpacing::Sparse)
426 .disabled(true)
427 .child(Label::new("No matches").color(Color::Muted)),
428 ),
429 )
430 })
431 .children(self.delegate.render_footer(cx))
432 }
433}