Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:38:32.327Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

picker.rs

1940 lines · 68.4 KB · rust
1use anyhow::Result;
2use gpui::Action;
3use gpui::{
4    AnyElement, App, Bounds, ClickEvent, Context, DismissEvent, EventEmitter, FocusHandle,
5    Focusable, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Pixels, ScrollStrategy,
6    Task, UniformListScrollHandle, Window, actions, canvas, div, list, prelude::*, uniform_list,
7};
8use head::Head;
9use schemars::JsonSchema;
10use serde::Deserialize;
11use std::{
12    cell::Cell, cell::RefCell, collections::HashMap, ops::Range, rc::Rc, sync::Arc, time::Duration,
13};
14use ui::{
15    Checkbox, ContextMenu, Divider, DocumentationAside, KeyBinding, PopoverMenuHandle, Tooltip,
16    prelude::*,
17};
18use ui_input::ErasedEditorEvent;
19use util::ResultExt;
20use workspace::ModalView;
21use zed_actions::editor::{MoveDown, MoveUp};
22
23mod footer;
24mod head;
25pub mod highlighted_match_with_paths;
26pub mod parts;
27mod persistence;
28pub mod popover_menu;
29mod preview;
30mod render;
31mod shape;
32
33use crate::shape::RelativeHeight;
34use crate::shape::RelativeWidth;
35pub use footer::PickerAction;
36pub use language::{HighlightedText, HighlightedTextBuilder};
37pub use preview::Layout as PreviewLayout;
38pub use preview::MatchLocation;
39pub use preview::Preview;
40pub use preview::PreviewBackend;
41pub use preview::PreviewSource;
42pub use preview::Update as PreviewUpdate;
43pub use ui_input::ErasedEditor;
44
45pub const DEFAULT_MODAL_WIDTH: Rems = Rems(34.0);
46pub const DEFAULT_MODAL_MAX_HEIGHT: Rems = Rems(24.0);
47
48enum ElementContainer {
49    List(ListState),
50    UniformList(UniformListScrollHandle),
51}
52
53pub enum Direction {
54    Up,
55    Down,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum ScrollBehavior {
60    RevealSelected,
61    PreserveOffset,
62}
63
64actions!(
65    picker,
66    [
67        /// Confirms the selected completion in the picker.
68        ConfirmCompletion,
69        /// Toggles the preview between hidden and visible.
70        TogglePreview,
71        /// Shows the preview to the right of the results.
72        SetPreviewRight,
73        /// Shows the preview below the results.
74        SetPreviewBelow,
75        /// Hides the preview.
76        SetPreviewHidden,
77        /// Opens the footer's actions menu.
78        ToggleActionsMenu,
79        /// Toggles multi-select mode, in which clicking items adds them to
80        /// the selection instead of opening them
81        ToggleMultiSelect,
82        /// Toggles the current item in the multi-selection and advances to
83        /// the next item, starting multi-select mode if it isn't already
84        /// active
85        MultiSelectNext,
86    ]
87);
88
89/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally,
90/// performing some kind of action on it.
91#[derive(Clone, PartialEq, Deserialize, JsonSchema, Default, Action)]
92#[action(namespace = picker)]
93#[serde(deny_unknown_fields)]
94pub struct ConfirmInput {
95    pub secondary: bool,
96}
97
98struct PendingUpdateMatches {
99    delegate_update_matches: Option<Task<()>>,
100    _task: Task<Result<()>>,
101}
102
103#[derive(Clone, Copy)]
104enum Presentation {
105    /// A self-contained modal: draws its own elevated background and dismisses
106    /// when it loses focus. May optionally be resized (persisting its size);
107    /// resizing only makes sense for modals.
108    Modal { resizable: bool },
109    /// A popover attached to a menu/trigger: draws its own elevated background
110    /// and dismisses when it loses focus, but is never resizable.
111    Popover,
112    /// Embedded inside a larger container (e.g. another modal) that provides its
113    /// own chrome and handles dismissal.
114    Embedded,
115}
116
117/// The default size for a given preview layout. With the preview hidden the
118/// picker uses its standard size; showing a preview expands it to the larger
119/// "telescope" size so the results pane isn't cramped beside the preview.
120fn default_shape_for_layout(hidden: shape::Centered, layout: preview::Layout) -> shape::Centered {
121    match layout {
122        preview::Layout::Hidden => hidden,
123        preview::Layout::Right | preview::Layout::Below => shape::Centered::default(),
124    }
125}
126
127pub struct Picker<D: PickerDelegate> {
128    pub delegate: D,
129    element_container: ElementContainer,
130    head: Head,
131    preview: Option<Preview>,
132    pending_update_matches: Option<PendingUpdateMatches>,
133    confirm_on_update: Option<bool>,
134    select_instead_of_open: bool,
135    shape: shape::Shape,
136    /// The size the picker opens at (and resets to). Defaults depend on whether
137    /// the picker has a preview; see [`Picker::initial_width`] / [`Picker::max_height`].
138    default_shape: shape::Centered,
139    size_bounds: shape::SizeBounds,
140    /// An external control to display a scrollbar in the `Picker`.
141    show_scrollbar: bool,
142    /// How the picker is presented, which controls its chrome and behavior.
143    presentation: Presentation,
144    /// Bounds tracking for the picker container (for aside positioning)
145    picker_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
146    /// Bounds tracking for items (for aside positioning) - maps item index to bounds
147    item_bounds: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
148    shape_loaded_from_persistence: bool,
149    /// Handle for the default footer's Actions popover menu. Used to keep the
150    /// picker open while that menu has focus.
151    actions_menu_handle: PopoverMenuHandle<ContextMenu>,
152    reopenable: bool,
153}
154
155#[derive(Debug, Default, Clone, Copy, PartialEq)]
156pub enum PickerEditorPosition {
157    #[default]
158    /// Render the editor at the start of the picker. Usually the top
159    Start,
160    /// Render the editor at the end of the picker. Usually the bottom
161    End,
162}
163
164pub trait PickerDelegate: Sized + 'static {
165    type ListItem: IntoElement;
166
167    /// Name of the picker, this is the key for serialization. We could use the
168    /// typename of the delegate but then a rename would break persistence.
169    fn name() -> &'static str;
170    fn match_count(&self) -> usize;
171    fn selected_index(&self) -> usize;
172    fn separators_after_indices(&self) -> Vec<usize> {
173        Vec::new()
174    }
175    fn set_selected_index(
176        &mut self,
177        ix: usize,
178        window: &mut Window,
179        cx: &mut Context<Picker<Self>>,
180    );
181
182    /// Called before the picker handles `SelectPrevious` or `SelectNext`. Return `Some(query)` to
183    /// set a new query and prevent the default selection behavior.
184    fn select_history(
185        &mut self,
186        _direction: Direction,
187        _query: &str,
188        _window: &mut Window,
189        _cx: &mut App,
190    ) -> Option<String> {
191        None
192    }
193    fn can_select(
194        &self,
195        _ix: usize,
196        _window: &mut Window,
197        _cx: &mut Context<Picker<Self>>,
198    ) -> bool {
199        true
200    }
201    fn select_on_hover(&self) -> bool {
202        true
203    }
204
205    // Allows binding some optional effect to when the selection changes.
206    fn selected_index_changed(
207        &self,
208        _ix: usize,
209        _window: &mut Window,
210        _cx: &mut Context<Picker<Self>>,
211    ) -> Option<Box<dyn Fn(&mut Window, &mut App) + 'static>> {
212        None
213    }
214    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
215    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
216        Some("No matches".into())
217    }
218    fn update_matches(
219        &mut self,
220        query: String,
221        window: &mut Window,
222        cx: &mut Context<Picker<Self>>,
223    ) -> Task<()>;
224
225    // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background
226    // work for up to `duration` to try and get a result synchronously.
227    // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes
228    // mostly work when dismissing a palette.
229    fn finalize_update_matches(
230        &mut self,
231        _query: String,
232        _duration: Duration,
233        _window: &mut Window,
234        _cx: &mut Context<Picker<Self>>,
235    ) -> bool {
236        false
237    }
238
239    /// Override if you want to have <enter> update the query instead of confirming.
240    fn confirm_update_query(
241        &mut self,
242        _window: &mut Window,
243        _cx: &mut Context<Picker<Self>>,
244    ) -> Option<String> {
245        None
246    }
247    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>);
248    /// Whether this delegate supports selecting multiple items at once. When
249    /// `true`, the delegate owns the multi-selection state; the picker only
250    /// drives the generic UX (toggle action, indicator, click routing).
251    fn supports_multi_select(&self) -> bool {
252        false
253    }
254    /// Whether the item at `ix` is part of the current multi-selection.
255    fn is_item_selected(&self, _ix: usize) -> bool {
256        false
257    }
258    /// Toggle whether the item at `ix` is part of the multi-selection. Items
259    /// that cannot participate in multi-selection (e.g. non-file entries)
260    /// should leave the selection unchanged.
261    fn toggle_item_selected(
262        &mut self,
263        _ix: usize,
264        _window: &mut Window,
265        _cx: &mut Context<Picker<Self>>,
266    ) {
267    }
268    /// Number of items currently in the multi-selection.
269    fn selected_item_count(&self) -> usize {
270        0
271    }
272    /// Clear the multi-selection.
273    fn clear_selection(&mut self, _cx: &mut Context<Picker<Self>>) {}
274    /// Open every item in the multi-selection. Called on confirm when the
275    /// selection is non-empty; implementations should clear the selection.
276    fn confirm_multi(
277        &mut self,
278        _secondary: bool,
279        _window: &mut Window,
280        _cx: &mut Context<Picker<Self>>,
281    ) {
282    }
283    /// Instead of interacting with currently selected entry, treats editor input literally,
284    /// performing some kind of action on it.
285    fn confirm_input(
286        &mut self,
287        _secondary: bool,
288        _window: &mut Window,
289        _: &mut Context<Picker<Self>>,
290    ) {
291    }
292    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>);
293    fn should_dismiss(&self) -> bool {
294        true
295    }
296    fn confirm_completion(
297        &mut self,
298        _query: String,
299        _window: &mut Window,
300        _: &mut Context<Picker<Self>>,
301    ) -> Option<String> {
302        None
303    }
304    /// Called when `SelectChild` fires (e.g. shift-right-arrow). Return `Some(query)`
305    /// to step into the currently selected item (e.g. a directory); the picker
306    /// will set the query and refresh matches.
307    fn select_child(
308        &mut self,
309        _window: &mut Window,
310        _cx: &mut Context<Picker<Self>>,
311    ) -> Option<String> {
312        None
313    }
314
315    /// Called when `SelectParent` fires (e.g. shift-left-arrow). Return `Some(query)`
316    /// to step back to the parent; the picker will set the query and refresh
317    /// matches.
318    fn select_parent(
319        &mut self,
320        _window: &mut Window,
321        _cx: &mut Context<Picker<Self>>,
322    ) -> Option<String> {
323        None
324    }
325
326    fn editor_position(&self) -> PickerEditorPosition {
327        PickerEditorPosition::default()
328    }
329
330    /// Prevent closing the modal on clicking in a popover menu that portrudes out
331    /// This is already set by the Actions menu from the picker, this is here to
332    /// support extra menus added by the delegate.
333    fn has_another_open_menu(&self, _window: &Window, _cx: &App) -> bool {
334        false
335    }
336
337    /// An optional control rendered at the trailing edge of the search bar, e.g.
338    /// a filter toggle. Returning `Some` is the easy way to add such a control;
339    /// for full control over the search bar, override [`Self::render_editor`].
340    fn searchbar_trailer(
341        &self,
342        _window: &mut Window,
343        _cx: &mut Context<Picker<Self>>,
344    ) -> Option<AnyElement> {
345        None
346    }
347
348    /// Overrides the search bar entirely. Most delegates should return `None`
349    /// to get the picker-rendered default (which includes
350    /// [`Self::searchbar_trailer`] and the multi-select toggle); override for
351    /// full control over the search bar.
352    fn render_editor(
353        &self,
354        _editor: &Arc<dyn ErasedEditor>,
355        _window: &mut Window,
356        _cx: &mut Context<Picker<Self>>,
357    ) -> Option<Div> {
358        None
359    }
360
361    fn try_get_preview_data_for_match(&self, _cx: &App) -> Option<PreviewUpdate> {
362        None
363    }
364
365    /// Called on the delegate when opening a preview to the side. Delegates can
366    /// then change how much space they use for rendering the match
367    fn preview_layout_changed(&mut self, _layout_is_horizontal: bool) {}
368
369    fn render_match(
370        &self,
371        ix: usize,
372        selected: bool,
373        window: &mut Window,
374        cx: &mut Context<Picker<Self>>,
375    ) -> Option<Self::ListItem>;
376
377    fn render_match_with_checkbox(
378        &self,
379        _ix: usize,
380        _selected: bool,
381        _checkbox: AnyElement,
382        _window: &mut Window,
383        _cx: &mut Context<Picker<Self>>,
384    ) -> Option<Self::ListItem> {
385        None
386    }
387
388    fn render_header(
389        &self,
390        _window: &mut Window,
391        _: &mut Context<Picker<Self>>,
392    ) -> Option<AnyElement> {
393        None
394    }
395
396    /// Overrides the picker's footer.
397    ///
398    /// Note there normally isn't a footer unless this is set or the picker has
399    /// a preview. If the picker has a preview add actions to it using picker_actions.
400    fn render_footer(
401        &self,
402        _window: &mut Window,
403        _: &mut Context<Picker<Self>>,
404    ) -> Option<AnyElement> {
405        None
406    }
407
408    /// Make this non-empty to have an `Actions` menu show up in the footer
409    fn actions_menu(
410        &self,
411        _window: &mut Window,
412        _cx: &mut Context<Picker<Self>>,
413    ) -> Vec<footer::PickerAction> {
414        Vec::new()
415    }
416
417    fn documentation_aside(
418        &self,
419        _window: &mut Window,
420        _cx: &mut Context<Picker<Self>>,
421    ) -> Option<DocumentationAside> {
422        None
423    }
424
425    /// Returns the index of the item whose documentation aside should be shown.
426    /// This is used to position the aside relative to that item.
427    /// Typically this is the hovered item, not necessarily the selected item.
428    fn documentation_aside_index(&self) -> Option<usize> {
429        None
430    }
431}
432
433impl<D: PickerDelegate> Focusable for Picker<D> {
434    fn focus_handle(&self, cx: &App) -> FocusHandle {
435        match &self.head {
436            Head::Editor(editor) => editor.focus_handle(cx),
437            Head::Empty(head) => head.focus_handle(cx),
438        }
439    }
440}
441
442#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
443enum ContainerKind {
444    List,
445    UniformList,
446}
447
448impl<D: PickerDelegate> Picker<D> {
449    /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
450    /// The picker allows the user to perform search items by text.
451    /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
452    pub fn uniform_list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
453        let head = Head::editor(
454            delegate.placeholder_text(window, cx),
455            Self::on_input_editor_event,
456            window,
457            cx,
458        );
459
460        Self::new(delegate, ContainerKind::UniformList, head, None, window, cx)
461    }
462
463    /// A picker similar to [`uniform_list()`](Self::uniform_list) however this picker has a
464    /// preview window where it shows extra information.
465    pub fn uniform_list_with_preview(
466        delegate: D,
467        preview: Arc<dyn PreviewBackend>,
468        window: &mut Window,
469        cx: &mut Context<Self>,
470    ) -> Self {
471        let head = Head::editor(
472            delegate.placeholder_text(window, cx),
473            Self::on_input_editor_event,
474            window,
475            cx,
476        );
477
478        let preview = Preview::new(preview);
479        Self::new(
480            delegate,
481            ContainerKind::UniformList,
482            head,
483            Some(preview),
484            window,
485            cx,
486        )
487    }
488
489    /// A picker similar to [`list()`](Self::list) (variable-height rows) but with
490    /// a preview window. Use this instead of [`uniform_list_with_preview()`](Self::uniform_list_with_preview)
491    /// when [`PickerDelegate::render_match`] can return rows of different heights
492    /// (e.g. section headers and separators interleaved with matches).
493    pub fn list_with_preview(
494        delegate: D,
495        preview: Arc<dyn PreviewBackend>,
496        window: &mut Window,
497        cx: &mut Context<Self>,
498    ) -> Self {
499        let head = Head::editor(
500            delegate.placeholder_text(window, cx),
501            Self::on_input_editor_event,
502            window,
503            cx,
504        );
505
506        let preview = Preview::new(preview);
507        Self::new(
508            delegate,
509            ContainerKind::List,
510            head,
511            Some(preview),
512            window,
513            cx,
514        )
515    }
516
517    /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
518    /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
519    pub fn nonsearchable_uniform_list(
520        delegate: D,
521        window: &mut Window,
522        cx: &mut Context<Self>,
523    ) -> Self {
524        let head = Head::empty(Self::on_empty_head_blur, window, cx);
525
526        Self::new(delegate, ContainerKind::UniformList, head, None, window, cx)
527    }
528
529    /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
530    /// The picker allows the user to perform search items by text.
531    /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
532    pub fn nonsearchable_list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
533        let head = Head::empty(Self::on_empty_head_blur, window, cx);
534
535        Self::new(delegate, ContainerKind::List, head, None, window, cx)
536    }
537
538    /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
539    /// The picker allows the user to perform search items by text.
540    /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
541    pub fn list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
542        let head = Head::editor(
543            delegate.placeholder_text(window, cx),
544            Self::on_input_editor_event,
545            window,
546            cx,
547        );
548
549        Self::new(delegate, ContainerKind::List, head, None, window, cx)
550    }
551
552    fn new(
553        delegate: D,
554        container: ContainerKind,
555        head: Head,
556        mut preview: Option<Preview>,
557        window: &mut Window,
558        cx: &mut Context<Self>,
559    ) -> Self {
560        let element_container = Self::create_element_container(container);
561        if let Some(preview) = &mut preview {
562            preview.layout = persistence::load_last_preview_layout(D::name(), cx)
563                .log_err()
564                .flatten()
565                .unwrap_or_default();
566        };
567        let has_preview = preview.is_some();
568        let persisted_shape =
569            persistence::try_load_shape(D::name(), preview.as_ref().map(|p| p.layout), cx)
570                .log_err()
571                .flatten();
572        // Every picker opens at the standard "simple" size: a fixed width and a
573        // standard max height it shrinks below when there's little content.
574        // Showing a preview expands it to the larger "telescope" size (see
575        // `default_shape_for_layout`).
576        let default_shape = shape::Centered::simple();
577        let initial_layout = preview
578            .as_ref()
579            .map(|p| p.layout)
580            .unwrap_or(preview::Layout::Hidden);
581        let mut size_bounds = shape::SizeBounds::default();
582        // For a plain picker the whole-picker minimum is just its opening width,
583        // so it can't be resized/clamped narrower than it opens. Preview pickers
584        // keep the standard per-pane minimums.
585        if !has_preview && let Some(width) = default_shape.width.as_rems() {
586            size_bounds.min_results.width = width;
587        }
588        let mut this = Self {
589            delegate,
590            head,
591            element_container,
592            pending_update_matches: None,
593            confirm_on_update: None,
594            select_instead_of_open: false,
595            preview,
596            shape_loaded_from_persistence: persisted_shape.is_some(),
597            shape: persisted_shape.unwrap_or_else(|| {
598                shape::Shape::HorizontallyCentered(default_shape_for_layout(
599                    default_shape,
600                    initial_layout,
601                ))
602            }),
603            default_shape,
604            show_scrollbar: false,
605            presentation: Presentation::Modal {
606                resizable: has_preview,
607            },
608            picker_bounds: Rc::new(Cell::new(None)),
609            item_bounds: Rc::new(RefCell::new(HashMap::default())),
610            size_bounds,
611            actions_menu_handle: PopoverMenuHandle::default(),
612            reopenable: true,
613        };
614        // give delegate the initial preview layout
615        this.delegate
616            .preview_layout_changed(matches!(initial_layout, preview::Layout::Right));
617        if this.reopenable {
618            let focus_handle = this.focus_handle(cx);
619            workspace::register_reopenable_picker(&focus_handle, cx);
620        }
621        this.update_matches("".to_string(), window, cx);
622        // give the delegate 4ms to render the first set of suggestions.
623        this.delegate
624            .finalize_update_matches("".to_string(), Duration::from_millis(4), window, cx);
625        this
626    }
627
628    fn create_element_container(container: ContainerKind) -> ElementContainer {
629        match container {
630            ContainerKind::UniformList => {
631                ElementContainer::UniformList(UniformListScrollHandle::new())
632            }
633            ContainerKind::List => {
634                ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.)))
635            }
636        }
637    }
638
639    /// Overrides the width the picker opens at (and resets to). Plain pickers
640    /// default to [`DEFAULT_MODAL_WIDTH`]; only call this for pickers that need a
641    /// different width (e.g. narrow popover selectors).
642    ///
643    /// For a plain (no-preview) picker the opening width is also its minimum, so
644    /// it can't be resized or clamped narrower than it opens.
645    pub fn initial_width(mut self, width: impl Into<RelativeWidth>) -> Self {
646        let width = width.into();
647        self.default_shape.width = width;
648        if self.live_shape_is_hidden_default() {
649            self.shape.set_initial_width(width);
650        }
651        // A plain picker's whole-picker minimum tracks its opening width. Preview
652        // pickers keep the standard per-pane minimums.
653        if self.preview.is_none()
654            && let Some(rems) = width.as_rems()
655        {
656            self.size_bounds.min_results.width = rems;
657        }
658        self
659    }
660
661    /// Overrides the picker's max height. Plain pickers default to
662    /// [`DEFAULT_MODAL_MAX_HEIGHT`] and shrink below it to fit their content;
663    /// only call this for pickers that want a different cap (e.g. the outline
664    /// view, which is taller).
665    pub fn max_height(mut self, height: impl Into<RelativeHeight>) -> Self {
666        let height = height.into();
667        self.default_shape.height = height;
668        if self.live_shape_is_hidden_default() {
669            self.shape.set_initial_height(height);
670        }
671        self
672    }
673
674    /// Whether the live shape still reflects the hidden-layout default, i.e. it
675    /// was not restored from a persisted size and currently opens with no
676    /// preview. Only then may `initial_width`/`max_height` push their
677    /// hidden/no-preview defaults onto it; a picker reopened in a preview layout
678    /// keeps the larger telescope default, which those (smaller) no-preview
679    /// values must not shrink.
680    fn live_shape_is_hidden_default(&self) -> bool {
681        !self.shape_loaded_from_persistence
682            && self.preview_layout().unwrap_or(preview::Layout::Hidden) == preview::Layout::Hidden
683    }
684
685    /// Whether the picker fills its full height (preview visible) or shrinks to
686    /// fit its content, treating the height as a maximum (no preview visible).
687    fn fill_height(&self) -> bool {
688        self.preview
689            .as_ref()
690            .is_some_and(|preview| preview.layout != preview::Layout::Hidden)
691    }
692
693    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
694        self.show_scrollbar = show_scrollbar;
695        self
696    }
697
698    /// Controls whether a modal hosting this picker can be revealed again with
699    /// `workspace::ReopenLastPicker` after it's dismissed. Defaults to `true`;
700    /// pass `false` to exclude this picker. As a builder, this only takes effect
701    /// while constructing the picker, before it's opened.
702    pub fn reopenable(mut self, reopenable: bool, cx: &mut App) -> Self {
703        if reopenable == self.reopenable {
704            return self;
705        }
706        self.reopenable = reopenable;
707        let focus_handle = self.focus_handle(cx);
708        if reopenable {
709            workspace::register_reopenable_picker(&focus_handle, cx);
710        } else {
711            workspace::deregister_reopenable_picker(&focus_handle, cx);
712        }
713        self
714    }
715
716    /// Presents the picker as embedded inside a larger container that provides
717    /// its own chrome and dismissal, rather than the default self-contained
718    /// modal. Use [`popover`](Self::popover) instead for menu-attached pickers.
719    pub fn embedded(mut self) -> Self {
720        self.presentation = Presentation::Embedded;
721        self
722    }
723
724    /// Presents the picker as a popover surface: it draws its own elevated
725    /// background (like a modal) but is not resizable. Use this for pickers
726    /// shown inside a menu/popover.
727    pub fn popover(mut self) -> Self {
728        self.set_popover();
729        self
730    }
731
732    pub(crate) fn set_popover(&mut self) {
733        self.presentation = Presentation::Popover;
734    }
735
736    /// Controls whether the user can drag to resize the picker (and whether its
737    /// size is persisted). Only applies to modal pickers; no-op otherwise.
738    /// Defaults to `true` for pickers with a preview and `false` otherwise.
739    pub fn resizable(mut self, resizable: bool) -> Self {
740        if let Presentation::Modal { resizable: r } = &mut self.presentation {
741            *r = resizable;
742        }
743        self
744    }
745
746    /// Whether the picker draws its own elevated background and dismisses on
747    /// blur (modals and popovers, but not embedded pickers).
748    fn draws_own_container(&self) -> bool {
749        matches!(
750            self.presentation,
751            Presentation::Modal { .. } | Presentation::Popover
752        )
753    }
754
755    /// Whether the picker can be resized (only ever true for modals).
756    fn is_resizable(&self) -> bool {
757        matches!(self.presentation, Presentation::Modal { resizable: true })
758    }
759
760    pub fn list_measure_all(mut self) -> Self {
761        match self.element_container {
762            ElementContainer::List(state) => {
763                self.element_container = ElementContainer::List(state.measure_all());
764            }
765            _ => {}
766        }
767        self
768    }
769
770    pub fn focus(&self, window: &mut Window, cx: &mut App) {
771        self.focus_handle(cx).focus(window, cx);
772    }
773
774    /// Handles the selecting an index, and passing the change to the delegate.
775    /// If `fallback_direction` is set to `None`, the index will not be selected
776    /// if the element at that index cannot be selected.
777    /// If `fallback_direction` is set to
778    /// `Some(..)`, the next selectable element will be selected in the
779    /// specified direction (Down or Up), cycling through all elements until
780    /// finding one that can be selected or returning if there are no selectable elements.
781    /// If `scroll_to_index` is true, the new selected index will be scrolled into
782    /// view.
783    ///
784    /// If some effect is bound to `selected_index_changed`, it will be executed.
785    pub fn set_selected_index(
786        &mut self,
787        mut ix: usize,
788        fallback_direction: Option<Direction>,
789        scroll_to_index: bool,
790        window: &mut Window,
791        cx: &mut Context<Self>,
792    ) {
793        let match_count = self.delegate.match_count();
794        if match_count == 0 {
795            return;
796        }
797
798        if let Some(bias) = fallback_direction {
799            let mut curr_ix = ix;
800            while !self.delegate.can_select(curr_ix, window, cx) {
801                curr_ix = match bias {
802                    Direction::Down => {
803                        if curr_ix == match_count - 1 {
804                            0
805                        } else {
806                            curr_ix + 1
807                        }
808                    }
809                    Direction::Up => {
810                        if curr_ix == 0 {
811                            match_count - 1
812                        } else {
813                            curr_ix - 1
814                        }
815                    }
816                };
817                // There is no item that can be selected
818                if ix == curr_ix {
819                    return;
820                }
821            }
822            ix = curr_ix;
823        } else if !self.delegate.can_select(ix, window, cx) {
824            return;
825        }
826
827        let previous_index = self.delegate.selected_index();
828        self.delegate.set_selected_index(ix, window, cx);
829        let current_index = self.delegate.selected_index();
830
831        if previous_index != current_index {
832            if let Some(action) = self.delegate.selected_index_changed(ix, window, cx) {
833                action(window, cx);
834            }
835            if let Some(preview) = &mut self.preview
836                && let Some(update) = self.delegate.try_get_preview_data_for_match(cx)
837            {
838                preview.update(update, window, cx);
839            }
840            if scroll_to_index {
841                self.scroll_to_item_index(ix);
842            }
843        }
844    }
845
846    pub fn select_next(
847        &mut self,
848        _: &menu::SelectNext,
849        window: &mut Window,
850        cx: &mut Context<Self>,
851    ) {
852        let query = self.query(cx);
853        if let Some(query) = self
854            .delegate
855            .select_history(Direction::Down, &query, window, cx)
856        {
857            self.set_query(&query, window, cx);
858            return;
859        }
860        let count = self.delegate.match_count();
861        if count > 0 {
862            let index = self.delegate.selected_index();
863            let ix = if index == count - 1 { 0 } else { index + 1 };
864            self.set_selected_index(ix, Some(Direction::Down), true, window, cx);
865            cx.notify();
866        }
867    }
868
869    pub fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
870        self.select_previous(&Default::default(), window, cx);
871    }
872
873    fn select_previous(
874        &mut self,
875        _: &menu::SelectPrevious,
876        window: &mut Window,
877        cx: &mut Context<Self>,
878    ) {
879        let query = self.query(cx);
880        if let Some(query) = self
881            .delegate
882            .select_history(Direction::Up, &query, window, cx)
883        {
884            self.set_query(&query, window, cx);
885            return;
886        }
887        let count = self.delegate.match_count();
888        if count > 0 {
889            let index = self.delegate.selected_index();
890            let ix = if index == 0 { count - 1 } else { index - 1 };
891            self.set_selected_index(ix, Some(Direction::Up), true, window, cx);
892            cx.notify();
893        }
894    }
895
896    pub fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
897        self.select_next(&Default::default(), window, cx);
898    }
899
900    pub fn select_first(
901        &mut self,
902        _: &menu::SelectFirst,
903        window: &mut Window,
904        cx: &mut Context<Self>,
905    ) {
906        let count = self.delegate.match_count();
907        if count > 0 {
908            self.set_selected_index(0, Some(Direction::Down), true, window, cx);
909            cx.notify();
910        }
911    }
912
913    fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
914        let count = self.delegate.match_count();
915        if count > 0 {
916            self.set_selected_index(count - 1, Some(Direction::Up), true, window, cx);
917            cx.notify();
918        }
919    }
920
921    pub fn cycle_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
922        let count = self.delegate.match_count();
923        let index = self.delegate.selected_index();
924        let new_index = if index + 1 == count { 0 } else { index + 1 };
925        self.set_selected_index(new_index, Some(Direction::Down), true, window, cx);
926        cx.notify();
927    }
928
929    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
930        if self.delegate.should_dismiss() {
931            self.select_instead_of_open = false;
932            self.delegate.clear_selection(cx);
933            self.delegate.dismissed(window, cx);
934            cx.emit(DismissEvent);
935        }
936    }
937
938    fn toggle_multi_select(
939        &mut self,
940        _: &ToggleMultiSelect,
941        _window: &mut Window,
942        cx: &mut Context<Self>,
943    ) {
944        if !self.delegate.supports_multi_select() {
945            cx.propagate();
946            return;
947        }
948        self.select_instead_of_open = !self.select_instead_of_open;
949        if !self.select_instead_of_open {
950            self.delegate.clear_selection(cx);
951        }
952        cx.notify();
953    }
954
955    fn multi_select_next(
956        &mut self,
957        _: &MultiSelectNext,
958        window: &mut Window,
959        cx: &mut Context<Self>,
960    ) {
961        // Propagate so `tab` retains its other meanings (e.g.
962        // `ConfirmCompletion`) in pickers without multi-select.
963        if !self.delegate.supports_multi_select() {
964            cx.propagate();
965            return;
966        }
967        self.select_instead_of_open = true;
968        let ix = self.delegate.selected_index();
969        self.delegate.toggle_item_selected(ix, window, cx);
970        self.select_next(&menu::SelectNext, window, cx);
971        cx.notify();
972    }
973
974    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
975        if self.pending_update_matches.is_some()
976            && !self.delegate.finalize_update_matches(
977                self.query(cx),
978                Duration::from_millis(16),
979                window,
980                cx,
981            )
982        {
983            self.confirm_on_update = Some(false)
984        } else {
985            self.pending_update_matches.take();
986            self.do_confirm(false, window, cx);
987        }
988    }
989
990    fn secondary_confirm(
991        &mut self,
992        _: &menu::SecondaryConfirm,
993        window: &mut Window,
994        cx: &mut Context<Self>,
995    ) {
996        if self.pending_update_matches.is_some()
997            && !self.delegate.finalize_update_matches(
998                self.query(cx),
999                Duration::from_millis(16),
1000                window,
1001                cx,
1002            )
1003        {
1004            self.confirm_on_update = Some(true)
1005        } else {
1006            self.do_confirm(true, window, cx);
1007        }
1008    }
1009
1010    fn confirm_input(&mut self, input: &ConfirmInput, window: &mut Window, cx: &mut Context<Self>) {
1011        self.delegate.confirm_input(input.secondary, window, cx);
1012    }
1013
1014    fn confirm_completion(
1015        &mut self,
1016        _: &ConfirmCompletion,
1017        window: &mut Window,
1018        cx: &mut Context<Self>,
1019    ) {
1020        if let Some(new_query) = self.delegate.confirm_completion(self.query(cx), window, cx) {
1021            self.set_query(&new_query, window, cx);
1022        } else {
1023            cx.propagate()
1024        }
1025    }
1026    fn select_child(&mut self, _: &menu::SelectChild, window: &mut Window, cx: &mut Context<Self>) {
1027        if let Some(new_query) = self.delegate.select_child(window, cx) {
1028            self.set_query(&new_query, window, cx);
1029        } else {
1030            cx.propagate()
1031        }
1032    }
1033
1034    fn select_parent(
1035        &mut self,
1036        _: &menu::SelectParent,
1037        window: &mut Window,
1038        cx: &mut Context<Self>,
1039    ) {
1040        if let Some(new_query) = self.delegate.select_parent(window, cx) {
1041            self.set_query(&new_query, window, cx);
1042        } else {
1043            cx.propagate()
1044        }
1045    }
1046
1047    fn set_preview_right(
1048        &mut self,
1049        _: &SetPreviewRight,
1050        window: &mut Window,
1051        cx: &mut Context<Self>,
1052    ) {
1053        self.set_preview_layout(preview::Layout::Right, window, cx);
1054    }
1055
1056    fn set_preview_below(
1057        &mut self,
1058        _: &SetPreviewBelow,
1059        window: &mut Window,
1060        cx: &mut Context<Self>,
1061    ) {
1062        self.set_preview_layout(preview::Layout::Below, window, cx);
1063    }
1064
1065    fn set_preview_hidden(
1066        &mut self,
1067        _: &SetPreviewHidden,
1068        window: &mut Window,
1069        cx: &mut Context<Self>,
1070    ) {
1071        self.set_preview_layout(preview::Layout::Hidden, window, cx);
1072    }
1073
1074    fn toggle_actions_menu(
1075        &mut self,
1076        _: &ToggleActionsMenu,
1077        window: &mut Window,
1078        cx: &mut Context<Self>,
1079    ) {
1080        self.actions_menu_handle.toggle(window, cx);
1081    }
1082
1083    fn handle_click(
1084        &mut self,
1085        ix: usize,
1086        secondary: bool,
1087        window: &mut Window,
1088        cx: &mut Context<Self>,
1089    ) {
1090        cx.stop_propagation();
1091        window.prevent_default();
1092        if !self.delegate.can_select(ix, window, cx) {
1093            return;
1094        }
1095        self.set_selected_index(ix, None, false, window, cx);
1096        if self.delegate.supports_multi_select() && (secondary || self.select_instead_of_open) {
1097            self.select_instead_of_open = true;
1098            self.delegate.toggle_item_selected(ix, window, cx);
1099            cx.notify();
1100        } else {
1101            self.do_confirm(secondary, window, cx);
1102        }
1103    }
1104
1105    fn do_confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Self>) {
1106        if self.delegate.supports_multi_select() && self.delegate.selected_item_count() > 0 {
1107            self.select_instead_of_open = false;
1108            self.delegate.confirm_multi(secondary, window, cx);
1109        } else if let Some(update_query) = self.delegate.confirm_update_query(window, cx) {
1110            self.set_query(&update_query, window, cx);
1111            self.set_selected_index(0, Some(Direction::Down), false, window, cx);
1112        } else {
1113            self.delegate.confirm(secondary, window, cx)
1114        }
1115    }
1116
1117    fn on_input_editor_event(
1118        &mut self,
1119        event: &ErasedEditorEvent,
1120        window: &mut Window,
1121        cx: &mut Context<Self>,
1122    ) {
1123        let Head::Editor(editor) = &self.head else {
1124            panic!("unexpected call");
1125        };
1126        match event {
1127            ErasedEditorEvent::BufferEdited => {
1128                let query = editor.text(cx);
1129                self.update_matches(query, window, cx);
1130            }
1131            ErasedEditorEvent::Blurred => {
1132                // Opening a footer/search-bar menu blurs the editor; don't
1133                // dismiss the picker while such a menu is open/focused.
1134                let menu_focused = self.actions_menu_handle.is_focused(window, cx)
1135                    || self.actions_menu_handle.is_deployed()
1136                    || self.delegate.has_another_open_menu(window, cx);
1137                if self.draws_own_container() && window.is_window_active() && !menu_focused {
1138                    self.cancel(&menu::Cancel, window, cx);
1139                }
1140            }
1141        }
1142    }
1143
1144    fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1145        let Head::Empty(_) = &self.head else {
1146            panic!("unexpected call");
1147        };
1148        if window.is_window_active() {
1149            self.cancel(&menu::Cancel, window, cx);
1150        }
1151    }
1152
1153    pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1154        match &self.head {
1155            Head::Editor(editor) => {
1156                let placeholder = self.delegate.placeholder_text(window, cx);
1157
1158                editor.set_placeholder_text(placeholder.as_ref(), window, cx);
1159                cx.notify();
1160            }
1161            Head::Empty(_) => {}
1162        }
1163    }
1164
1165    pub fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1166        let query = self.query(cx);
1167        self.update_matches(query, window, cx);
1168    }
1169
1170    pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
1171        self.update_matches_with_options(query, ScrollBehavior::RevealSelected, window, cx);
1172    }
1173
1174    pub fn update_matches_with_options(
1175        &mut self,
1176        query: String,
1177        scroll_behavior: ScrollBehavior,
1178        window: &mut Window,
1179        cx: &mut Context<Self>,
1180    ) {
1181        let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx);
1182
1183        self.matches_updated(scroll_behavior, window, cx);
1184        // This struct ensures that we can synchronously drop the task returned by the
1185        // delegate's `update_matches` method and the task that the picker is spawning.
1186        // If we simply capture the delegate's task into the picker's task, when the picker's
1187        // task gets synchronously dropped, the delegate's task would keep running until
1188        // the picker's task has a chance of being scheduled, because dropping a task happens
1189        // asynchronously.
1190        self.pending_update_matches = Some(PendingUpdateMatches {
1191            delegate_update_matches: Some(delegate_pending_update_matches),
1192            _task: cx.spawn_in(window, async move |this, cx| {
1193                let delegate_pending_update_matches = this.update(cx, |this, _| {
1194                    this.pending_update_matches
1195                        .as_mut()
1196                        .unwrap()
1197                        .delegate_update_matches
1198                        .take()
1199                        .unwrap()
1200                })?;
1201                delegate_pending_update_matches.await;
1202                this.update_in(cx, |this, window, cx| {
1203                    this.matches_updated(scroll_behavior, window, cx);
1204                })
1205            }),
1206        });
1207    }
1208
1209    fn matches_updated(
1210        &mut self,
1211        scroll_behavior: ScrollBehavior,
1212        window: &mut Window,
1213        cx: &mut Context<Self>,
1214    ) {
1215        let match_count = self.delegate.match_count();
1216        if match_count == 0
1217            && let Some(preview) = &mut self.preview
1218        {
1219            preview.clear(cx)
1220        }
1221
1222        match &mut self.element_container {
1223            ElementContainer::List(state) => match scroll_behavior {
1224                ScrollBehavior::RevealSelected => {
1225                    state.reset(match_count);
1226                    let index = self.delegate.selected_index();
1227                    self.scroll_to_item_index(index);
1228                }
1229                ScrollBehavior::PreserveOffset => {
1230                    let offset = state.logical_scroll_top();
1231                    state.reset(match_count);
1232                    state.scroll_to(offset);
1233                }
1234            },
1235            ElementContainer::UniformList(_) => match scroll_behavior {
1236                ScrollBehavior::RevealSelected => {
1237                    let index = self.delegate.selected_index();
1238                    self.scroll_to_item_index(index);
1239                }
1240                ScrollBehavior::PreserveOffset => {}
1241            },
1242        }
1243        self.pending_update_matches = None;
1244        if let Some(update) = self.delegate.try_get_preview_data_for_match(cx)
1245            && let Some(preview) = &mut self.preview
1246        {
1247            preview.update(update, window, cx);
1248        }
1249        if let Some(secondary) = self.confirm_on_update.take() {
1250            self.do_confirm(secondary, window, cx);
1251        }
1252        cx.notify();
1253    }
1254
1255    pub fn query(&self, cx: &App) -> String {
1256        match &self.head {
1257            Head::Editor(editor) => editor.text(cx),
1258            Head::Empty(_) => "".to_string(),
1259        }
1260    }
1261
1262    pub fn set_query(&self, query: &str, window: &mut Window, cx: &mut App) {
1263        if let Head::Editor(editor) = &self.head {
1264            editor.set_text(query, window, cx);
1265            editor.move_selection_to_end(window, cx);
1266        }
1267    }
1268
1269    /// Selects the entire query, so the next keystroke replaces it (and a single
1270    /// backspace clears it). Matches the buffer search bar's seeded-query behavior.
1271    pub fn select_query(&self, window: &mut Window, cx: &mut App) {
1272        if let Head::Editor(editor) = &self.head {
1273            editor.select_all(window, cx);
1274        }
1275    }
1276
1277    fn scroll_to_item_index(&mut self, ix: usize) {
1278        match &mut self.element_container {
1279            ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
1280            ElementContainer::UniformList(scroll_handle) => {
1281                scroll_handle.scroll_to_item(ix, ScrollStrategy::Nearest)
1282            }
1283        }
1284    }
1285
1286    pub fn is_scrolled_to_end(&self) -> Option<bool> {
1287        match &self.element_container {
1288            ElementContainer::List(state) => state.is_scrolled_to_end(),
1289            ElementContainer::UniformList(scroll_handle) => scroll_handle.is_scrolled_to_end(),
1290        }
1291    }
1292
1293    fn render_element(
1294        &self,
1295        window: &mut Window,
1296        cx: &mut Context<Self>,
1297        ix: usize,
1298    ) -> impl IntoElement + use<D> {
1299        let item_bounds = self.item_bounds.clone();
1300        let selectable =
1301            ix < self.delegate.match_count() && self.delegate.can_select(ix, window, cx);
1302
1303        let supports_multi_select = self.delegate.supports_multi_select();
1304        let is_multi_selected = supports_multi_select && self.delegate.is_item_selected(ix);
1305        let multi_select_active = supports_multi_select && self.select_instead_of_open;
1306
1307        let item_with_checkbox = if multi_select_active && selectable {
1308            let checkbox = self
1309                .render_multi_select_indicator(ix, is_multi_selected, cx)
1310                .into_any_element();
1311            self.delegate.render_match_with_checkbox(
1312                ix,
1313                ix == self.delegate.selected_index(),
1314                checkbox,
1315                window,
1316                cx,
1317            )
1318        } else {
1319            None
1320        };
1321
1322        let use_fallback_indicator =
1323            multi_select_active && selectable && item_with_checkbox.is_none();
1324        let focus_handle = self.focus_handle(cx);
1325
1326        div()
1327            .id(("item", ix))
1328            .when(selectable, |this| this.cursor_pointer())
1329            .when(use_fallback_indicator, |this| {
1330                this.hover(|s| s.bg(cx.theme().colors().ghost_element_hover))
1331            })
1332            .when(multi_select_active && selectable, |this| {
1333                this.tooltip(Tooltip::element(move |_window, cx| {
1334                    h_flex()
1335                        .gap_2()
1336                        .child(
1337                            h_flex()
1338                                .gap_1()
1339                                .child(KeyBinding::for_action_in(
1340                                    &MultiSelectNext,
1341                                    &focus_handle,
1342                                    cx,
1343                                ))
1344                                .child(Label::new("Select")),
1345                        )
1346                        .child(Divider::vertical())
1347                        .child(
1348                            h_flex()
1349                                .gap_1()
1350                                .child(KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx))
1351                                .child(Label::new("Open")),
1352                        )
1353                        .into_any_element()
1354                }))
1355            })
1356            .child(
1357                canvas(
1358                    move |bounds, _window, _cx| {
1359                        item_bounds.borrow_mut().insert(ix, bounds);
1360                    },
1361                    |_bounds, _state, _window, _cx| {},
1362                )
1363                .size_full()
1364                .absolute()
1365                .top_0()
1366                .left_0(),
1367            )
1368            .when(!self.delegate.select_on_hover(), |this| {
1369                this.on_mouse_down(MouseButton::Left, |_, window, _cx| {
1370                    window.prevent_default();
1371                })
1372            })
1373            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
1374                this.handle_click(ix, event.modifiers().secondary(), window, cx)
1375            }))
1376            // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
1377            // and produces right mouse button events. This matches platforms norms
1378            // but means that UIs which depend on holding ctrl down (such as the tab
1379            // switcher) can't be clicked on. Hence, this handler.
1380            .on_mouse_up(
1381                MouseButton::Right,
1382                cx.listener(move |this, event: &MouseUpEvent, window, cx| {
1383                    // We specifically want to use the platform key here, as
1384                    // ctrl will already be held down for the tab switcher.
1385                    this.handle_click(ix, event.modifiers.platform, window, cx)
1386                }),
1387            )
1388            .when(self.delegate.select_on_hover(), |this| {
1389                this.on_hover(cx.listener(move |this, hovered: &bool, window, cx| {
1390                    if *hovered {
1391                        this.set_selected_index(ix, None, false, window, cx);
1392                        cx.notify();
1393                    }
1394                }))
1395            })
1396            .map(|row| {
1397                if let Some(item) = item_with_checkbox {
1398                    row.child(item)
1399                } else if supports_multi_select {
1400                    row.child(
1401                        h_flex()
1402                            // Headers and separators cannot be part of the
1403                            // selection, so they get no indicator.
1404                            .when(use_fallback_indicator, |this| {
1405                                this.child(self.render_multi_select_indicator(
1406                                    ix,
1407                                    is_multi_selected,
1408                                    cx,
1409                                ))
1410                            })
1411                            .children(self.delegate.render_match(
1412                                ix,
1413                                ix == self.delegate.selected_index(),
1414                                window,
1415                                cx,
1416                            )),
1417                    )
1418                } else {
1419                    row.children(self.delegate.render_match(
1420                        ix,
1421                        ix == self.delegate.selected_index(),
1422                        window,
1423                        cx,
1424                    ))
1425                }
1426            })
1427            .when(
1428                self.delegate.separators_after_indices().contains(&ix),
1429                |picker| {
1430                    picker
1431                        .border_color(cx.theme().colors().border_variant)
1432                        .border_b_1()
1433                        .py(px(-1.0))
1434                },
1435            )
1436    }
1437
1438    fn render_multi_select_indicator(
1439        &self,
1440        ix: usize,
1441        is_selected: bool,
1442        cx: &mut Context<Self>,
1443    ) -> impl IntoElement {
1444        Checkbox::new(("picker-multi-select-checkbox", ix), is_selected.into())
1445            .fill()
1446            .elevation(ui::ElevationIndex::ModalSurface)
1447            .on_click(cx.listener(move |this, _: &ui::ToggleState, window, cx| {
1448                // Don't let the row's click handler also toggle the item.
1449                cx.stop_propagation();
1450                this.delegate.toggle_item_selected(ix, window, cx);
1451                cx.notify();
1452            }))
1453    }
1454
1455    fn render_element_container(&self, cx: &mut Context<Self>) -> impl IntoElement {
1456        // When the picker shrinks to fit its content, the list infers its size
1457        // from its items. When it fills its full height (preview visible), the
1458        // list fills the available space.
1459        let sizing_behavior = if self.fill_height() {
1460            ListSizingBehavior::Auto
1461        } else {
1462            ListSizingBehavior::Infer
1463        };
1464
1465        match &self.element_container {
1466            ElementContainer::UniformList(scroll_handle) => uniform_list(
1467                "candidates",
1468                self.delegate.match_count(),
1469                cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
1470                    visible_range
1471                        .map(|ix| picker.render_element(window, cx, ix))
1472                        .collect()
1473                }),
1474            )
1475            .with_sizing_behavior(sizing_behavior)
1476            .flex_grow_1()
1477            .py_1()
1478            .track_scroll(&scroll_handle)
1479            .into_any_element(),
1480            ElementContainer::List(state) => list(
1481                state.clone(),
1482                cx.processor(|this, ix, window, cx| {
1483                    this.render_element(window, cx, ix).into_any_element()
1484                }),
1485            )
1486            .with_sizing_behavior(sizing_behavior)
1487            .flex_grow_1()
1488            .py(DynamicSpacing::Base04.rems(cx))
1489            .into_any_element(),
1490        }
1491    }
1492
1493    #[cfg(any(test, feature = "test-support"))]
1494    pub fn logical_scroll_top_index(&self) -> usize {
1495        match &self.element_container {
1496            ElementContainer::List(state) => state.logical_scroll_top().item_ix,
1497            ElementContainer::UniformList(scroll_handle) => {
1498                scroll_handle.logical_scroll_top_index()
1499            }
1500        }
1501    }
1502
1503    fn preview_layout(&self) -> Option<preview::Layout> {
1504        self.preview.as_ref().map(|p| p.layout)
1505    }
1506    fn is_auto_vertical(&self, window: &Window) -> bool {
1507        self.preview_layout() == Some(preview::Layout::Right)
1508            && self
1509                .size_bounds
1510                .would_clamp_width_if_horizontal(&self.shape, window)
1511    }
1512    /// To check whether we're rendering vertically instead of
1513    /// horizontally due to the auto override
1514    fn preview_layout_rendered(&self, window: &Window) -> Option<preview::Layout> {
1515        let would_clamp = matches!(
1516            self.preview,
1517            Some(Preview {
1518                layout: preview::Layout::Right,
1519                ..
1520            })
1521        ) && self.is_auto_vertical(window);
1522        if would_clamp {
1523            Some(preview::Layout::Below)
1524        } else {
1525            self.preview_layout()
1526        }
1527    }
1528
1529    #[cfg(any(test, feature = "test-support"))]
1530    pub fn results_width(&self, window: &Window) -> gpui::Pixels {
1531        let layout = self
1532            .preview_layout_rendered(window)
1533            .unwrap_or(preview::Layout::Hidden);
1534        let pos = self
1535            .shape
1536            .results_position_and_size(layout, &self.size_bounds, window);
1537        pos.right - pos.left
1538    }
1539
1540    fn toggle_preview(&mut self, _: &TogglePreview, window: &mut Window, cx: &mut Context<Self>) {
1541        self.toggle_preview_visible(window, cx);
1542    }
1543
1544    fn toggle_preview_visible(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1545        let next = match self.preview_layout() {
1546            Some(preview::Layout::Hidden) | None => preview::Layout::Right,
1547            Some(_) => preview::Layout::Hidden,
1548        };
1549        self.set_preview_layout(next, window, cx);
1550    }
1551
1552    fn set_preview_layout(
1553        &mut self,
1554        layout: preview::Layout,
1555        _window: &mut Window,
1556        cx: &mut Context<Self>,
1557    ) {
1558        persistence::store_last_layout(D::name(), self.preview.as_ref().map(|_| layout), cx);
1559
1560        let Some(preview) = &mut self.preview else {
1561            return;
1562        };
1563        preview.layout = layout;
1564        // Restore the size the user last left this layout at, or fall back to the
1565        // layout's default (simple when hidden, larger when a preview is shown).
1566        self.shape = persistence::try_load_shape(D::name(), layout, cx)
1567            .log_err()
1568            .flatten()
1569            .unwrap_or_else(|| {
1570                shape::Shape::HorizontallyCentered(default_shape_for_layout(
1571                    self.default_shape,
1572                    layout,
1573                ))
1574            });
1575        self.delegate
1576            .preview_layout_changed(matches!(layout, preview::Layout::Right));
1577        cx.notify();
1578    }
1579}
1580
1581#[cfg(test)]
1582mod tests {
1583    use super::*;
1584    use gpui::TestAppContext;
1585    use std::cell::Cell;
1586
1587    struct TestDelegate {
1588        items: Vec<bool>,
1589        selected_index: usize,
1590        confirmed_index: Rc<Cell<Option<usize>>>,
1591        supports_multi_select: bool,
1592        selected_items: Vec<usize>,
1593        multi_confirmed: Rc<Cell<Option<Vec<usize>>>>,
1594    }
1595
1596    impl TestDelegate {
1597        fn new(items: Vec<bool>) -> Self {
1598            Self {
1599                items,
1600                selected_index: 0,
1601                confirmed_index: Rc::new(Cell::new(None)),
1602                supports_multi_select: false,
1603                selected_items: Vec::new(),
1604                multi_confirmed: Rc::new(Cell::new(None)),
1605            }
1606        }
1607
1608        fn with_multi_select(mut self) -> Self {
1609            self.supports_multi_select = true;
1610            self
1611        }
1612    }
1613
1614    impl PickerDelegate for TestDelegate {
1615        type ListItem = ui::ListItem;
1616
1617        fn name() -> &'static str {
1618            "test"
1619        }
1620
1621        fn match_count(&self) -> usize {
1622            self.items.len()
1623        }
1624
1625        fn selected_index(&self) -> usize {
1626            self.selected_index
1627        }
1628
1629        fn set_selected_index(
1630            &mut self,
1631            ix: usize,
1632            _window: &mut Window,
1633            _cx: &mut Context<Picker<Self>>,
1634        ) {
1635            self.selected_index = ix;
1636        }
1637
1638        fn can_select(
1639            &self,
1640            ix: usize,
1641            _window: &mut Window,
1642            _cx: &mut Context<Picker<Self>>,
1643        ) -> bool {
1644            self.items.get(ix).copied().unwrap_or(false)
1645        }
1646
1647        fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1648            "Test".into()
1649        }
1650
1651        fn update_matches(
1652            &mut self,
1653            _query: String,
1654            _window: &mut Window,
1655            _cx: &mut Context<Picker<Self>>,
1656        ) -> Task<()> {
1657            Task::ready(())
1658        }
1659
1660        fn confirm(
1661            &mut self,
1662            _secondary: bool,
1663            _window: &mut Window,
1664            _cx: &mut Context<Picker<Self>>,
1665        ) {
1666            self.confirmed_index.set(Some(self.selected_index));
1667        }
1668
1669        fn supports_multi_select(&self) -> bool {
1670            self.supports_multi_select
1671        }
1672
1673        fn is_item_selected(&self, ix: usize) -> bool {
1674            self.selected_items.contains(&ix)
1675        }
1676
1677        fn toggle_item_selected(
1678            &mut self,
1679            ix: usize,
1680            _window: &mut Window,
1681            _cx: &mut Context<Picker<Self>>,
1682        ) {
1683            if let Some(position) = self.selected_items.iter().position(|&item| item == ix) {
1684                self.selected_items.remove(position);
1685            } else {
1686                self.selected_items.push(ix);
1687            }
1688        }
1689
1690        fn selected_item_count(&self) -> usize {
1691            self.selected_items.len()
1692        }
1693
1694        fn clear_selection(&mut self, _cx: &mut Context<Picker<Self>>) {
1695            self.selected_items.clear();
1696        }
1697
1698        fn confirm_multi(
1699            &mut self,
1700            _secondary: bool,
1701            _window: &mut Window,
1702            _cx: &mut Context<Picker<Self>>,
1703        ) {
1704            self.multi_confirmed
1705                .set(Some(std::mem::take(&mut self.selected_items)));
1706        }
1707
1708        fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
1709
1710        fn render_match(
1711            &self,
1712            ix: usize,
1713            selected: bool,
1714            _window: &mut Window,
1715            _cx: &mut Context<Picker<Self>>,
1716        ) -> Option<Self::ListItem> {
1717            Some(
1718                ui::ListItem::new(ix)
1719                    .inset(true)
1720                    .toggle_state(selected)
1721                    .child(ui::Label::new(format!("Item {ix}"))),
1722            )
1723        }
1724    }
1725
1726    fn init_test(cx: &mut TestAppContext) {
1727        cx.update(|cx| {
1728            let store = settings::SettingsStore::test(cx);
1729            cx.set_global(store);
1730            theme_settings::init(theme::LoadThemes::JustBase, cx);
1731            editor::init(cx);
1732        });
1733    }
1734
1735    #[gpui::test]
1736    async fn test_clicking_non_selectable_item_does_not_confirm(cx: &mut TestAppContext) {
1737        init_test(cx);
1738
1739        let confirmed_index = Rc::new(Cell::new(None));
1740        let (picker, cx) = cx.add_window_view(|window, cx| {
1741            let mut delegate = TestDelegate::new(vec![true, false, true]);
1742            delegate.confirmed_index = confirmed_index.clone();
1743            Picker::uniform_list(delegate, window, cx)
1744        });
1745
1746        picker.update(cx, |picker, _cx| {
1747            assert_eq!(picker.delegate.selected_index(), 0);
1748        });
1749
1750        picker.update_in(cx, |picker, window, cx| {
1751            picker.handle_click(1, false, window, cx);
1752        });
1753        assert!(
1754            confirmed_index.get().is_none(),
1755            "clicking a non-selectable item should not confirm"
1756        );
1757
1758        picker.update_in(cx, |picker, window, cx| {
1759            picker.handle_click(0, false, window, cx);
1760        });
1761        assert_eq!(
1762            confirmed_index.get(),
1763            Some(0),
1764            "clicking a selectable item should confirm"
1765        );
1766    }
1767
1768    #[gpui::test]
1769    async fn test_keyboard_navigation_skips_non_selectable_items(cx: &mut TestAppContext) {
1770        init_test(cx);
1771
1772        let (picker, cx) = cx.add_window_view(|window, cx| {
1773            Picker::uniform_list(TestDelegate::new(vec![true, false, true]), window, cx)
1774        });
1775
1776        picker.update(cx, |picker, _cx| {
1777            assert_eq!(picker.delegate.selected_index(), 0);
1778        });
1779
1780        picker.update_in(cx, |picker, window, cx| {
1781            picker.select_next(&menu::SelectNext, window, cx);
1782        });
1783        picker.update(cx, |picker, _cx| {
1784            assert_eq!(
1785                picker.delegate.selected_index(),
1786                2,
1787                "select_next should skip non-selectable item at index 1"
1788            );
1789        });
1790
1791        picker.update_in(cx, |picker, window, cx| {
1792            picker.select_previous(&menu::SelectPrevious, window, cx);
1793        });
1794        picker.update(cx, |picker, _cx| {
1795            assert_eq!(
1796                picker.delegate.selected_index(),
1797                0,
1798                "select_previous should skip non-selectable item at index 1"
1799            );
1800        });
1801    }
1802
1803    #[gpui::test]
1804    async fn test_multi_select_mode_routes_clicks(cx: &mut TestAppContext) {
1805        init_test(cx);
1806
1807        let confirmed_index = Rc::new(Cell::new(None));
1808        let multi_confirmed = Rc::new(Cell::new(None));
1809        let (picker, cx) = cx.add_window_view(|window, cx| {
1810            let mut delegate = TestDelegate::new(vec![true, true, true]).with_multi_select();
1811            delegate.confirmed_index = confirmed_index.clone();
1812            delegate.multi_confirmed = multi_confirmed.clone();
1813            Picker::uniform_list(delegate, window, cx)
1814        });
1815
1816        // A plain click confirms just like in any picker.
1817        picker.update_in(cx, |picker, window, cx| {
1818            picker.handle_click(1, false, window, cx);
1819        });
1820        assert_eq!(confirmed_index.take(), Some(1));
1821        picker.update(cx, |picker, _cx| {
1822            assert_eq!(picker.delegate.selected_item_count(), 0);
1823        });
1824
1825        // A secondary (cmd) click starts multi-select mode and toggles the
1826        // clicked item into the selection instead of confirming.
1827        picker.update_in(cx, |picker, window, cx| {
1828            picker.handle_click(2, true, window, cx);
1829        });
1830        assert_eq!(confirmed_index.take(), None, "cmd+click must not confirm");
1831        picker.update(cx, |picker, _cx| {
1832            assert!(
1833                picker.select_instead_of_open,
1834                "cmd+click should start multi-select mode"
1835            );
1836            assert!(picker.delegate.is_item_selected(2));
1837        });
1838
1839        // While the mode is on, plain clicks toggle items too.
1840        picker.update_in(cx, |picker, window, cx| {
1841            picker.handle_click(0, false, window, cx);
1842        });
1843        assert_eq!(
1844            confirmed_index.take(),
1845            None,
1846            "in-mode clicks must not confirm"
1847        );
1848        picker.update(cx, |picker, _cx| {
1849            assert!(picker.delegate.is_item_selected(0));
1850            assert!(picker.delegate.is_item_selected(2));
1851            assert!(!picker.delegate.is_item_selected(1));
1852        });
1853
1854        // Confirming opens the whole selection and exits the mode.
1855        picker.update_in(cx, |picker, window, cx| {
1856            picker.do_confirm(false, window, cx);
1857        });
1858        assert_eq!(multi_confirmed.take(), Some(vec![2, 0]));
1859        picker.update_in(cx, |picker, window, cx| {
1860            picker.handle_click(1, false, window, cx);
1861        });
1862        assert_eq!(
1863            confirmed_index.take(),
1864            Some(1),
1865            "the mode should be off again after confirming"
1866        );
1867    }
1868
1869    #[gpui::test]
1870    async fn test_multi_select_next_starts_multi_select_mode(cx: &mut TestAppContext) {
1871        init_test(cx);
1872
1873        let (picker, cx) = cx.add_window_view(|window, cx| {
1874            Picker::uniform_list(
1875                TestDelegate::new(vec![true, true, true]).with_multi_select(),
1876                window,
1877                cx,
1878            )
1879        });
1880
1881        picker.update_in(cx, |picker, window, cx| {
1882            picker.multi_select_next(&MultiSelectNext, window, cx);
1883        });
1884        picker.update(cx, |picker, _cx| {
1885            assert!(
1886                picker.select_instead_of_open,
1887                "selecting an item should start multi-select mode"
1888            );
1889            assert!(picker.delegate.is_item_selected(0));
1890            assert_eq!(
1891                picker.delegate.selected_index(),
1892                1,
1893                "selecting should advance the cursor to the next item"
1894            );
1895        });
1896
1897        // In pickers without multi-select the action does nothing.
1898        let (plain_picker, cx) = cx.add_window_view(|window, cx| {
1899            Picker::uniform_list(TestDelegate::new(vec![true, true]), window, cx)
1900        });
1901        plain_picker.update_in(cx, |picker, window, cx| {
1902            picker.multi_select_next(&MultiSelectNext, window, cx);
1903        });
1904        plain_picker.update(cx, |picker, _cx| {
1905            assert!(!picker.select_instead_of_open);
1906            assert_eq!(picker.delegate.selected_item_count(), 0);
1907        });
1908    }
1909
1910    #[gpui::test]
1911    async fn test_exiting_multi_select_mode_clears_selection(cx: &mut TestAppContext) {
1912        init_test(cx);
1913
1914        let (picker, cx) = cx.add_window_view(|window, cx| {
1915            Picker::uniform_list(
1916                TestDelegate::new(vec![true, true]).with_multi_select(),
1917                window,
1918                cx,
1919            )
1920        });
1921
1922        picker.update_in(cx, |picker, window, cx| {
1923            picker.toggle_multi_select(&ToggleMultiSelect, window, cx);
1924            picker.handle_click(0, false, window, cx);
1925            picker.toggle_multi_select(&ToggleMultiSelect, window, cx);
1926        });
1927        picker.update(cx, |picker, _cx| {
1928            assert_eq!(
1929                picker.delegate.selected_item_count(),
1930                0,
1931                "leaving the mode should clear the selection"
1932            );
1933        });
1934    }
1935}
1936
1937impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
1938impl<D: PickerDelegate> ModalView for Picker<D> {}
1939impl<D: PickerDelegate> ui::FluentBuilder for Picker<D> {}
1940
Served at tenant.openagents/omega Member data and write actions are omitted.