picker.rs

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