1use anyhow::Result;
2use editor::{scroll::Autoscroll, Editor};
3use gpui::{
4 actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent,
5 DismissEvent, EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton,
6 MouseUpEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
7};
8use head::Head;
9use serde::Deserialize;
10use std::{sync::Arc, time::Duration};
11use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing};
12use workspace::ModalView;
13
14mod head;
15pub mod highlighted_match_with_paths;
16
17enum ElementContainer {
18 List(ListState),
19 UniformList(UniformListScrollHandle),
20}
21
22actions!(picker, [UseSelectedQuery]);
23
24/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally,
25/// performing some kind of action on it.
26#[derive(PartialEq, Clone, Deserialize, Default)]
27pub struct ConfirmInput {
28 pub secondary: bool,
29}
30
31impl_actions!(picker, [ConfirmInput]);
32
33struct PendingUpdateMatches {
34 delegate_update_matches: Option<Task<()>>,
35 _task: Task<Result<()>>,
36}
37
38pub struct Picker<D: PickerDelegate> {
39 pub delegate: D,
40 element_container: ElementContainer,
41 head: Head,
42 pending_update_matches: Option<PendingUpdateMatches>,
43 confirm_on_update: Option<bool>,
44 width: Option<Length>,
45 max_height: Option<Length>,
46
47 /// Whether the `Picker` is rendered as a self-contained modal.
48 ///
49 /// Set this to `false` when rendering the `Picker` as part of a larger modal.
50 is_modal: bool,
51}
52
53pub trait PickerDelegate: Sized + 'static {
54 type ListItem: IntoElement;
55
56 fn match_count(&self) -> usize;
57 fn selected_index(&self) -> usize;
58 fn separators_after_indices(&self) -> Vec<usize> {
59 Vec::new()
60 }
61 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
62 // Allows binding some optional effect to when the selection changes.
63 fn selected_index_changed(
64 &self,
65 _ix: usize,
66 _cx: &mut ViewContext<Picker<Self>>,
67 ) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
68 None
69 }
70 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str>;
71 fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
72 "No matches".into()
73 }
74 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
75
76 // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background
77 // work for up to `duration` to try and get a result synchronously.
78 // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes
79 // mostly work when dismissing a palette.
80 fn finalize_update_matches(
81 &mut self,
82 _query: String,
83 _duration: Duration,
84 _cx: &mut ViewContext<Picker<Self>>,
85 ) -> bool {
86 false
87 }
88
89 fn confirm_update_query(&mut self, _cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
90 None
91 }
92
93 fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
94 /// Instead of interacting with currently selected entry, treats editor input literally,
95 /// performing some kind of action on it.
96 fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext<Picker<Self>>) {}
97 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
98 fn should_dismiss(&self) -> bool {
99 true
100 }
101 fn selected_as_query(&self) -> Option<String> {
102 None
103 }
104
105 fn render_match(
106 &self,
107 ix: usize,
108 selected: bool,
109 cx: &mut ViewContext<Picker<Self>>,
110 ) -> Option<Self::ListItem>;
111 fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
112 None
113 }
114 fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
115 None
116 }
117}
118
119impl<D: PickerDelegate> FocusableView for Picker<D> {
120 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
121 match &self.head {
122 Head::Editor(editor) => editor.focus_handle(cx),
123 Head::Empty(head) => head.focus_handle(cx),
124 }
125 }
126}
127
128#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
129enum ContainerKind {
130 List,
131 UniformList,
132}
133
134impl<D: PickerDelegate> Picker<D> {
135 /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
136 /// The picker allows the user to perform search items by text.
137 /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
138 pub fn uniform_list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
139 let head = Head::editor(
140 delegate.placeholder_text(cx),
141 Self::on_input_editor_event,
142 cx,
143 );
144
145 Self::new(delegate, ContainerKind::UniformList, head, cx)
146 }
147
148 /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
149 /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
150 pub fn nonsearchable_uniform_list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
151 let head = Head::empty(Self::on_empty_head_blur, cx);
152
153 Self::new(delegate, ContainerKind::UniformList, head, cx)
154 }
155
156 /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
157 /// The picker allows the user to perform search items by text.
158 /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
159 pub fn list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
160 let head = Head::editor(
161 delegate.placeholder_text(cx),
162 Self::on_input_editor_event,
163 cx,
164 );
165
166 Self::new(delegate, ContainerKind::List, head, cx)
167 }
168
169 fn new(delegate: D, container: ContainerKind, head: Head, cx: &mut ViewContext<Self>) -> Self {
170 let mut this = Self {
171 delegate,
172 head,
173 element_container: Self::create_element_container(container, cx),
174 pending_update_matches: None,
175 confirm_on_update: None,
176 width: None,
177 max_height: None,
178 is_modal: true,
179 };
180 this.update_matches("".to_string(), cx);
181 // give the delegate 4ms to render the first set of suggestions.
182 this.delegate
183 .finalize_update_matches("".to_string(), Duration::from_millis(4), cx);
184 this
185 }
186
187 fn create_element_container(
188 container: ContainerKind,
189 cx: &mut ViewContext<Self>,
190 ) -> ElementContainer {
191 match container {
192 ContainerKind::UniformList => {
193 ElementContainer::UniformList(UniformListScrollHandle::new())
194 }
195 ContainerKind::List => {
196 let view = cx.view().downgrade();
197 ElementContainer::List(ListState::new(
198 0,
199 gpui::ListAlignment::Top,
200 px(1000.),
201 move |ix, cx| {
202 view.upgrade()
203 .map(|view| {
204 view.update(cx, |this, cx| {
205 this.render_element(cx, ix).into_any_element()
206 })
207 })
208 .unwrap_or_else(|| div().into_any_element())
209 },
210 ))
211 }
212 }
213 }
214
215 pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
216 self.width = Some(width.into());
217 self
218 }
219
220 pub fn max_height(mut self, max_height: impl Into<gpui::Length>) -> Self {
221 self.max_height = Some(max_height.into());
222 self
223 }
224
225 pub fn modal(mut self, modal: bool) -> Self {
226 self.is_modal = modal;
227 self
228 }
229
230 pub fn focus(&self, cx: &mut WindowContext) {
231 self.focus_handle(cx).focus(cx);
232 }
233
234 /// Handles the selecting an index, and passing the change to the delegate.
235 /// If `scroll_to_index` is true, the new selected index will be scrolled into view.
236 ///
237 /// If some effect is bound to `selected_index_changed`, it will be executed.
238 fn set_selected_index(&mut self, ix: usize, scroll_to_index: bool, cx: &mut ViewContext<Self>) {
239 let previous_index = self.delegate.selected_index();
240 self.delegate.set_selected_index(ix, cx);
241 let current_index = self.delegate.selected_index();
242
243 if previous_index != current_index {
244 if let Some(action) = self.delegate.selected_index_changed(ix, cx) {
245 action(cx);
246 }
247 if scroll_to_index {
248 self.scroll_to_item_index(ix);
249 }
250 }
251 }
252
253 pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
254 let count = self.delegate.match_count();
255 if count > 0 {
256 let index = self.delegate.selected_index();
257 let ix = if index == count - 1 { 0 } else { index + 1 };
258 self.set_selected_index(ix, true, cx);
259 cx.notify();
260 }
261 }
262
263 fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
264 let count = self.delegate.match_count();
265 if count > 0 {
266 let index = self.delegate.selected_index();
267 let ix = if index == 0 { count - 1 } else { index - 1 };
268 self.set_selected_index(ix, true, cx);
269 cx.notify();
270 }
271 }
272
273 fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
274 let count = self.delegate.match_count();
275 if count > 0 {
276 self.set_selected_index(0, true, cx);
277 cx.notify();
278 }
279 }
280
281 fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
282 let count = self.delegate.match_count();
283 if count > 0 {
284 self.delegate.set_selected_index(count - 1, cx);
285 self.set_selected_index(count - 1, true, cx);
286 cx.notify();
287 }
288 }
289
290 pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
291 let count = self.delegate.match_count();
292 let index = self.delegate.selected_index();
293 let new_index = if index + 1 == count { 0 } else { index + 1 };
294 self.set_selected_index(new_index, false, cx);
295 cx.notify();
296 }
297
298 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
299 if self.delegate.should_dismiss() {
300 self.delegate.dismissed(cx);
301 cx.emit(DismissEvent);
302 }
303 }
304
305 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
306 if self.pending_update_matches.is_some()
307 && !self
308 .delegate
309 .finalize_update_matches(self.query(cx), Duration::from_millis(16), cx)
310 {
311 self.confirm_on_update = Some(false)
312 } else {
313 self.pending_update_matches.take();
314 self.do_confirm(false, cx);
315 }
316 }
317
318 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
319 if self.pending_update_matches.is_some()
320 && !self
321 .delegate
322 .finalize_update_matches(self.query(cx), Duration::from_millis(16), cx)
323 {
324 self.confirm_on_update = Some(true)
325 } else {
326 self.do_confirm(true, cx);
327 }
328 }
329
330 fn confirm_input(&mut self, input: &ConfirmInput, cx: &mut ViewContext<Self>) {
331 self.delegate.confirm_input(input.secondary, cx);
332 }
333
334 fn use_selected_query(&mut self, _: &UseSelectedQuery, cx: &mut ViewContext<Self>) {
335 if let Some(new_query) = self.delegate.selected_as_query() {
336 self.set_query(new_query, cx);
337 cx.stop_propagation();
338 }
339 }
340
341 fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
342 cx.stop_propagation();
343 cx.prevent_default();
344 self.set_selected_index(ix, false, cx);
345 self.do_confirm(secondary, cx)
346 }
347
348 fn do_confirm(&mut self, secondary: bool, cx: &mut ViewContext<Self>) {
349 if let Some(update_query) = self.delegate.confirm_update_query(cx) {
350 self.set_query(update_query, cx);
351 self.delegate.set_selected_index(0, cx);
352 } else {
353 self.delegate.confirm(secondary, cx)
354 }
355 }
356
357 fn on_input_editor_event(
358 &mut self,
359 _: View<Editor>,
360 event: &editor::EditorEvent,
361 cx: &mut ViewContext<Self>,
362 ) {
363 let Head::Editor(ref editor) = &self.head else {
364 panic!("unexpected call");
365 };
366 match event {
367 editor::EditorEvent::BufferEdited => {
368 let query = editor.read(cx).text(cx);
369 self.update_matches(query, cx);
370 }
371 editor::EditorEvent::Blurred => {
372 self.cancel(&menu::Cancel, cx);
373 }
374 _ => {}
375 }
376 }
377
378 fn on_empty_head_blur(&mut self, cx: &mut ViewContext<Self>) {
379 let Head::Empty(_) = &self.head else {
380 panic!("unexpected call");
381 };
382 self.cancel(&menu::Cancel, cx);
383 }
384
385 pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
386 let query = self.query(cx);
387 self.update_matches(query, cx);
388 }
389
390 pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
391 let delegate_pending_update_matches = self.delegate.update_matches(query, cx);
392
393 self.matches_updated(cx);
394 // This struct ensures that we can synchronously drop the task returned by the
395 // delegate's `update_matches` method and the task that the picker is spawning.
396 // If we simply capture the delegate's task into the picker's task, when the picker's
397 // task gets synchronously dropped, the delegate's task would keep running until
398 // the picker's task has a chance of being scheduled, because dropping a task happens
399 // asynchronously.
400 self.pending_update_matches = Some(PendingUpdateMatches {
401 delegate_update_matches: Some(delegate_pending_update_matches),
402 _task: cx.spawn(|this, mut cx| async move {
403 let delegate_pending_update_matches = this.update(&mut cx, |this, _| {
404 this.pending_update_matches
405 .as_mut()
406 .unwrap()
407 .delegate_update_matches
408 .take()
409 .unwrap()
410 })?;
411 delegate_pending_update_matches.await;
412 this.update(&mut cx, |this, cx| {
413 this.matches_updated(cx);
414 })
415 }),
416 });
417 }
418
419 fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
420 if let ElementContainer::List(state) = &mut self.element_container {
421 state.reset(self.delegate.match_count());
422 }
423
424 let index = self.delegate.selected_index();
425 self.scroll_to_item_index(index);
426 self.pending_update_matches = None;
427 if let Some(secondary) = self.confirm_on_update.take() {
428 self.do_confirm(secondary, cx);
429 }
430 cx.notify();
431 }
432
433 pub fn query(&self, cx: &AppContext) -> String {
434 match &self.head {
435 Head::Editor(editor) => editor.read(cx).text(cx),
436 Head::Empty(_) => "".to_string(),
437 }
438 }
439
440 pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
441 if let Head::Editor(ref editor) = &self.head {
442 editor.update(cx, |editor, cx| {
443 editor.set_text(query, cx);
444 let editor_offset = editor.buffer().read(cx).len(cx);
445 editor.change_selections(Some(Autoscroll::Next), cx, |s| {
446 s.select_ranges(Some(editor_offset..editor_offset))
447 });
448 });
449 }
450 }
451
452 fn scroll_to_item_index(&mut self, ix: usize) {
453 match &mut self.element_container {
454 ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
455 ElementContainer::UniformList(scroll_handle) => scroll_handle.scroll_to_item(ix),
456 }
457 }
458
459 fn render_element(&self, cx: &mut ViewContext<Self>, ix: usize) -> impl IntoElement {
460 div()
461 .id(("item", ix))
462 .cursor_pointer()
463 .on_click(cx.listener(move |this, event: &ClickEvent, cx| {
464 this.handle_click(ix, event.down.modifiers.secondary(), cx)
465 }))
466 // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
467 // and produces right mouse button events. This matches platforms norms
468 // but means that UIs which depend on holding ctrl down (such as the tab
469 // switcher) can't be clicked on. Hence, this handler.
470 .on_mouse_up(
471 MouseButton::Right,
472 cx.listener(move |this, event: &MouseUpEvent, cx| {
473 // We specficially want to use the platform key here, as
474 // ctrl will already be held down for the tab switcher.
475 this.handle_click(ix, event.modifiers.platform, cx)
476 }),
477 )
478 .children(
479 self.delegate
480 .render_match(ix, ix == self.delegate.selected_index(), cx),
481 )
482 .when(
483 self.delegate.separators_after_indices().contains(&ix),
484 |picker| {
485 picker
486 .border_color(cx.theme().colors().border_variant)
487 .border_b_1()
488 .pb(px(-1.0))
489 },
490 )
491 }
492
493 fn render_element_container(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
494 match &self.element_container {
495 ElementContainer::UniformList(scroll_handle) => uniform_list(
496 cx.view().clone(),
497 "candidates",
498 self.delegate.match_count(),
499 move |picker, visible_range, cx| {
500 visible_range
501 .map(|ix| picker.render_element(cx, ix))
502 .collect()
503 },
504 )
505 .py_2()
506 .track_scroll(scroll_handle.clone())
507 .into_any_element(),
508 ElementContainer::List(state) => list(state.clone())
509 .with_sizing_behavior(gpui::ListSizingBehavior::Infer)
510 .py_2()
511 .into_any_element(),
512 }
513 }
514}
515
516impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
517impl<D: PickerDelegate> ModalView for Picker<D> {}
518
519impl<D: PickerDelegate> Render for Picker<D> {
520 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
521 div()
522 .key_context("Picker")
523 .size_full()
524 .when_some(self.width, |el, width| el.w(width))
525 .overflow_hidden()
526 // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
527 // as a part of a modal rather than the entire modal.
528 //
529 // We should revisit how the `Picker` is styled to make it more composable.
530 .when(self.is_modal, |this| this.elevation_3(cx))
531 .on_action(cx.listener(Self::select_next))
532 .on_action(cx.listener(Self::select_prev))
533 .on_action(cx.listener(Self::select_first))
534 .on_action(cx.listener(Self::select_last))
535 .on_action(cx.listener(Self::cancel))
536 .on_action(cx.listener(Self::confirm))
537 .on_action(cx.listener(Self::secondary_confirm))
538 .on_action(cx.listener(Self::use_selected_query))
539 .on_action(cx.listener(Self::confirm_input))
540 .child(match &self.head {
541 Head::Editor(editor) => v_flex()
542 .child(
543 h_flex()
544 .overflow_hidden()
545 .flex_none()
546 .h_9()
547 .px_4()
548 .child(editor.clone()),
549 )
550 .child(Divider::horizontal()),
551 Head::Empty(empty_head) => div().child(empty_head.clone()),
552 })
553 .when(self.delegate.match_count() > 0, |el| {
554 el.child(
555 v_flex()
556 .flex_grow()
557 .max_h(self.max_height.unwrap_or(rems(18.).into()))
558 .overflow_hidden()
559 .children(self.delegate.render_header(cx))
560 .child(self.render_element_container(cx)),
561 )
562 })
563 .when(self.delegate.match_count() == 0, |el| {
564 el.child(
565 v_flex().flex_grow().py_2().child(
566 ListItem::new("empty_state")
567 .inset(true)
568 .spacing(ListItemSpacing::Sparse)
569 .disabled(true)
570 .child(
571 Label::new(self.delegate.no_matches_text(cx)).color(Color::Muted),
572 ),
573 ),
574 )
575 })
576 .children(self.delegate.render_footer(cx))
577 }
578}