picker.rs

   1mod head;
   2pub mod highlighted_match_with_paths;
   3pub mod popover_menu;
   4
   5use anyhow::Result;
   6
   7use gpui::{
   8    Action, AnyElement, App, Bounds, ClickEvent, Context, DismissEvent, EventEmitter, FocusHandle,
   9    Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Pixels, Render,
  10    ScrollStrategy, Task, UniformListScrollHandle, Window, actions, canvas, div, list, prelude::*,
  11    uniform_list,
  12};
  13use head::Head;
  14use schemars::JsonSchema;
  15use serde::Deserialize;
  16use std::{
  17    cell::Cell, cell::RefCell, collections::HashMap, ops::Range, rc::Rc, sync::Arc, time::Duration,
  18};
  19use theme::ThemeSettings;
  20use ui::{
  21    Color, Divider, DocumentationAside, DocumentationSide, Label, ListItem, ListItemSpacing,
  22    ScrollAxes, Scrollbars, WithScrollbar, prelude::*, utils::WithRemSize, v_flex,
  23};
  24use ui_input::{ErasedEditor, ErasedEditorEvent};
  25use workspace::{ModalView, item::Settings};
  26use zed_actions::editor::{MoveDown, MoveUp};
  27
  28enum ElementContainer {
  29    List(ListState),
  30    UniformList(UniformListScrollHandle),
  31}
  32
  33pub enum Direction {
  34    Up,
  35    Down,
  36}
  37
  38actions!(
  39    picker,
  40    [
  41        /// Confirms the selected completion in the picker.
  42        ConfirmCompletion
  43    ]
  44);
  45
  46/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally,
  47/// performing some kind of action on it.
  48#[derive(Clone, PartialEq, Deserialize, JsonSchema, Default, Action)]
  49#[action(namespace = picker)]
  50#[serde(deny_unknown_fields)]
  51pub struct ConfirmInput {
  52    pub secondary: bool,
  53}
  54
  55struct PendingUpdateMatches {
  56    delegate_update_matches: Option<Task<()>>,
  57    _task: Task<Result<()>>,
  58}
  59
  60pub struct Picker<D: PickerDelegate> {
  61    pub delegate: D,
  62    element_container: ElementContainer,
  63    head: Head,
  64    pending_update_matches: Option<PendingUpdateMatches>,
  65    confirm_on_update: Option<bool>,
  66    width: Option<Length>,
  67    widest_item: Option<usize>,
  68    max_height: Option<Length>,
  69    /// An external control to display a scrollbar in the `Picker`.
  70    show_scrollbar: bool,
  71    /// Whether the `Picker` is rendered as a self-contained modal.
  72    ///
  73    /// Set this to `false` when rendering the `Picker` as part of a larger modal.
  74    is_modal: bool,
  75    /// Bounds tracking for the picker container (for aside positioning)
  76    picker_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
  77    /// Bounds tracking for items (for aside positioning) - maps item index to bounds
  78    item_bounds: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
  79}
  80
  81#[derive(Debug, Default, Clone, Copy, PartialEq)]
  82pub enum PickerEditorPosition {
  83    #[default]
  84    /// Render the editor at the start of the picker. Usually the top
  85    Start,
  86    /// Render the editor at the end of the picker. Usually the bottom
  87    End,
  88}
  89
  90pub trait PickerDelegate: Sized + 'static {
  91    type ListItem: IntoElement;
  92
  93    fn match_count(&self) -> usize;
  94    fn selected_index(&self) -> usize;
  95    fn separators_after_indices(&self) -> Vec<usize> {
  96        Vec::new()
  97    }
  98    fn set_selected_index(
  99        &mut self,
 100        ix: usize,
 101        window: &mut Window,
 102        cx: &mut Context<Picker<Self>>,
 103    );
 104
 105    /// Called before the picker handles `SelectPrevious` or `SelectNext`. Return `Some(query)` to
 106    /// set a new query and prevent the default selection behavior.
 107    fn select_history(
 108        &mut self,
 109        _direction: Direction,
 110        _query: &str,
 111        _window: &mut Window,
 112        _cx: &mut App,
 113    ) -> Option<String> {
 114        None
 115    }
 116    fn can_select(
 117        &self,
 118        _ix: usize,
 119        _window: &mut Window,
 120        _cx: &mut Context<Picker<Self>>,
 121    ) -> bool {
 122        true
 123    }
 124
 125    // Allows binding some optional effect to when the selection changes.
 126    fn selected_index_changed(
 127        &self,
 128        _ix: usize,
 129        _window: &mut Window,
 130        _cx: &mut Context<Picker<Self>>,
 131    ) -> Option<Box<dyn Fn(&mut Window, &mut App) + 'static>> {
 132        None
 133    }
 134    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
 135    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
 136        Some("No matches".into())
 137    }
 138    fn update_matches(
 139        &mut self,
 140        query: String,
 141        window: &mut Window,
 142        cx: &mut Context<Picker<Self>>,
 143    ) -> Task<()>;
 144
 145    // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background
 146    // work for up to `duration` to try and get a result synchronously.
 147    // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes
 148    // mostly work when dismissing a palette.
 149    fn finalize_update_matches(
 150        &mut self,
 151        _query: String,
 152        _duration: Duration,
 153        _window: &mut Window,
 154        _cx: &mut Context<Picker<Self>>,
 155    ) -> bool {
 156        false
 157    }
 158
 159    /// Override if you want to have <enter> update the query instead of confirming.
 160    fn confirm_update_query(
 161        &mut self,
 162        _window: &mut Window,
 163        _cx: &mut Context<Picker<Self>>,
 164    ) -> Option<String> {
 165        None
 166    }
 167    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>);
 168    /// Instead of interacting with currently selected entry, treats editor input literally,
 169    /// performing some kind of action on it.
 170    fn confirm_input(
 171        &mut self,
 172        _secondary: bool,
 173        _window: &mut Window,
 174        _: &mut Context<Picker<Self>>,
 175    ) {
 176    }
 177    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>);
 178    fn should_dismiss(&self) -> bool {
 179        true
 180    }
 181    fn confirm_completion(
 182        &mut self,
 183        _query: String,
 184        _window: &mut Window,
 185        _: &mut Context<Picker<Self>>,
 186    ) -> Option<String> {
 187        None
 188    }
 189
 190    fn editor_position(&self) -> PickerEditorPosition {
 191        PickerEditorPosition::default()
 192    }
 193
 194    fn render_editor(
 195        &self,
 196        editor: &Arc<dyn ErasedEditor>,
 197        window: &mut Window,
 198        cx: &mut Context<Picker<Self>>,
 199    ) -> Div {
 200        v_flex()
 201            .when(
 202                self.editor_position() == PickerEditorPosition::End,
 203                |this| this.child(Divider::horizontal()),
 204            )
 205            .child(
 206                h_flex()
 207                    .overflow_hidden()
 208                    .flex_none()
 209                    .h_9()
 210                    .px_2p5()
 211                    .child(editor.render(window, cx)),
 212            )
 213            .when(
 214                self.editor_position() == PickerEditorPosition::Start,
 215                |this| this.child(Divider::horizontal()),
 216            )
 217    }
 218
 219    fn render_match(
 220        &self,
 221        ix: usize,
 222        selected: bool,
 223        window: &mut Window,
 224        cx: &mut Context<Picker<Self>>,
 225    ) -> Option<Self::ListItem>;
 226
 227    fn render_header(
 228        &self,
 229        _window: &mut Window,
 230        _: &mut Context<Picker<Self>>,
 231    ) -> Option<AnyElement> {
 232        None
 233    }
 234
 235    fn render_footer(
 236        &self,
 237        _window: &mut Window,
 238        _: &mut Context<Picker<Self>>,
 239    ) -> Option<AnyElement> {
 240        None
 241    }
 242
 243    fn documentation_aside(
 244        &self,
 245        _window: &mut Window,
 246        _cx: &mut Context<Picker<Self>>,
 247    ) -> Option<DocumentationAside> {
 248        None
 249    }
 250
 251    /// Returns the index of the item whose documentation aside should be shown.
 252    /// This is used to position the aside relative to that item.
 253    /// Typically this is the hovered item, not necessarily the selected item.
 254    fn documentation_aside_index(&self) -> Option<usize> {
 255        None
 256    }
 257}
 258
 259impl<D: PickerDelegate> Focusable for Picker<D> {
 260    fn focus_handle(&self, cx: &App) -> FocusHandle {
 261        match &self.head {
 262            Head::Editor(editor) => editor.focus_handle(cx),
 263            Head::Empty(head) => head.focus_handle(cx),
 264        }
 265    }
 266}
 267
 268#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 269enum ContainerKind {
 270    List,
 271    UniformList,
 272}
 273
 274impl<D: PickerDelegate> Picker<D> {
 275    /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
 276    /// The picker allows the user to perform search items by text.
 277    /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
 278    pub fn uniform_list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
 279        let head = Head::editor(
 280            delegate.placeholder_text(window, cx),
 281            Self::on_input_editor_event,
 282            window,
 283            cx,
 284        );
 285
 286        Self::new(delegate, ContainerKind::UniformList, head, window, cx)
 287    }
 288
 289    /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
 290    /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
 291    pub fn nonsearchable_uniform_list(
 292        delegate: D,
 293        window: &mut Window,
 294        cx: &mut Context<Self>,
 295    ) -> Self {
 296        let head = Head::empty(Self::on_empty_head_blur, window, cx);
 297
 298        Self::new(delegate, ContainerKind::UniformList, head, window, cx)
 299    }
 300
 301    /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
 302    /// The picker allows the user to perform search items by text.
 303    /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
 304    pub fn nonsearchable_list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
 305        let head = Head::empty(Self::on_empty_head_blur, window, cx);
 306
 307        Self::new(delegate, ContainerKind::List, head, window, cx)
 308    }
 309
 310    /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
 311    /// The picker allows the user to perform search items by text.
 312    /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
 313    pub fn list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
 314        let head = Head::editor(
 315            delegate.placeholder_text(window, cx),
 316            Self::on_input_editor_event,
 317            window,
 318            cx,
 319        );
 320
 321        Self::new(delegate, ContainerKind::List, head, window, cx)
 322    }
 323
 324    fn new(
 325        delegate: D,
 326        container: ContainerKind,
 327        head: Head,
 328        window: &mut Window,
 329        cx: &mut Context<Self>,
 330    ) -> Self {
 331        let element_container = Self::create_element_container(container);
 332        let mut this = Self {
 333            delegate,
 334            head,
 335            element_container,
 336            pending_update_matches: None,
 337            confirm_on_update: None,
 338            width: None,
 339            widest_item: None,
 340            max_height: Some(rems(24.).into()),
 341            show_scrollbar: false,
 342            is_modal: true,
 343            picker_bounds: Rc::new(Cell::new(None)),
 344            item_bounds: Rc::new(RefCell::new(HashMap::default())),
 345        };
 346        this.update_matches("".to_string(), window, cx);
 347        // give the delegate 4ms to render the first set of suggestions.
 348        this.delegate
 349            .finalize_update_matches("".to_string(), Duration::from_millis(4), window, cx);
 350        this
 351    }
 352
 353    fn create_element_container(container: ContainerKind) -> ElementContainer {
 354        match container {
 355            ContainerKind::UniformList => {
 356                ElementContainer::UniformList(UniformListScrollHandle::new())
 357            }
 358            ContainerKind::List => {
 359                ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.)))
 360            }
 361        }
 362    }
 363
 364    pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
 365        self.width = Some(width.into());
 366        self
 367    }
 368
 369    pub fn widest_item(mut self, ix: Option<usize>) -> Self {
 370        self.widest_item = ix;
 371        self
 372    }
 373
 374    pub fn max_height(mut self, max_height: Option<gpui::Length>) -> Self {
 375        self.max_height = max_height;
 376        self
 377    }
 378
 379    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
 380        self.show_scrollbar = show_scrollbar;
 381        self
 382    }
 383
 384    pub fn modal(mut self, modal: bool) -> Self {
 385        self.is_modal = modal;
 386        self
 387    }
 388
 389    pub fn list_measure_all(mut self) -> Self {
 390        match self.element_container {
 391            ElementContainer::List(state) => {
 392                self.element_container = ElementContainer::List(state.measure_all());
 393            }
 394            _ => {}
 395        }
 396        self
 397    }
 398
 399    pub fn focus(&self, window: &mut Window, cx: &mut App) {
 400        self.focus_handle(cx).focus(window, cx);
 401    }
 402
 403    /// Handles the selecting an index, and passing the change to the delegate.
 404    /// If `fallback_direction` is set to `None`, the index will not be selected
 405    /// if the element at that index cannot be selected.
 406    /// If `fallback_direction` is set to
 407    /// `Some(..)`, the next selectable element will be selected in the
 408    /// specified direction (Down or Up), cycling through all elements until
 409    /// finding one that can be selected or returning if there are no selectable elements.
 410    /// If `scroll_to_index` is true, the new selected index will be scrolled into
 411    /// view.
 412    ///
 413    /// If some effect is bound to `selected_index_changed`, it will be executed.
 414    pub fn set_selected_index(
 415        &mut self,
 416        mut ix: usize,
 417        fallback_direction: Option<Direction>,
 418        scroll_to_index: bool,
 419        window: &mut Window,
 420        cx: &mut Context<Self>,
 421    ) {
 422        let match_count = self.delegate.match_count();
 423        if match_count == 0 {
 424            return;
 425        }
 426
 427        if let Some(bias) = fallback_direction {
 428            let mut curr_ix = ix;
 429            while !self.delegate.can_select(curr_ix, window, cx) {
 430                curr_ix = match bias {
 431                    Direction::Down => {
 432                        if curr_ix == match_count - 1 {
 433                            0
 434                        } else {
 435                            curr_ix + 1
 436                        }
 437                    }
 438                    Direction::Up => {
 439                        if curr_ix == 0 {
 440                            match_count - 1
 441                        } else {
 442                            curr_ix - 1
 443                        }
 444                    }
 445                };
 446                // There is no item that can be selected
 447                if ix == curr_ix {
 448                    return;
 449                }
 450            }
 451            ix = curr_ix;
 452        } else if !self.delegate.can_select(ix, window, cx) {
 453            return;
 454        }
 455
 456        let previous_index = self.delegate.selected_index();
 457        self.delegate.set_selected_index(ix, window, cx);
 458        let current_index = self.delegate.selected_index();
 459
 460        if previous_index != current_index {
 461            if let Some(action) = self.delegate.selected_index_changed(ix, window, cx) {
 462                action(window, cx);
 463            }
 464            if scroll_to_index {
 465                self.scroll_to_item_index(ix);
 466            }
 467        }
 468    }
 469
 470    pub fn select_next(
 471        &mut self,
 472        _: &menu::SelectNext,
 473        window: &mut Window,
 474        cx: &mut Context<Self>,
 475    ) {
 476        let query = self.query(cx);
 477        if let Some(query) = self
 478            .delegate
 479            .select_history(Direction::Down, &query, window, cx)
 480        {
 481            self.set_query(&query, window, cx);
 482            return;
 483        }
 484        let count = self.delegate.match_count();
 485        if count > 0 {
 486            let index = self.delegate.selected_index();
 487            let ix = if index == count - 1 { 0 } else { index + 1 };
 488            self.set_selected_index(ix, Some(Direction::Down), true, window, cx);
 489            cx.notify();
 490        }
 491    }
 492
 493    pub fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 494        self.select_previous(&Default::default(), window, cx);
 495    }
 496
 497    fn select_previous(
 498        &mut self,
 499        _: &menu::SelectPrevious,
 500        window: &mut Window,
 501        cx: &mut Context<Self>,
 502    ) {
 503        let query = self.query(cx);
 504        if let Some(query) = self
 505            .delegate
 506            .select_history(Direction::Up, &query, window, cx)
 507        {
 508            self.set_query(&query, window, cx);
 509            return;
 510        }
 511        let count = self.delegate.match_count();
 512        if count > 0 {
 513            let index = self.delegate.selected_index();
 514            let ix = if index == 0 { count - 1 } else { index - 1 };
 515            self.set_selected_index(ix, Some(Direction::Up), true, window, cx);
 516            cx.notify();
 517        }
 518    }
 519
 520    pub fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
 521        self.select_next(&Default::default(), window, cx);
 522    }
 523
 524    pub fn select_first(
 525        &mut self,
 526        _: &menu::SelectFirst,
 527        window: &mut Window,
 528        cx: &mut Context<Self>,
 529    ) {
 530        let count = self.delegate.match_count();
 531        if count > 0 {
 532            self.set_selected_index(0, Some(Direction::Down), true, window, cx);
 533            cx.notify();
 534        }
 535    }
 536
 537    fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
 538        let count = self.delegate.match_count();
 539        if count > 0 {
 540            self.set_selected_index(count - 1, Some(Direction::Up), true, window, cx);
 541            cx.notify();
 542        }
 543    }
 544
 545    pub fn cycle_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 546        let count = self.delegate.match_count();
 547        let index = self.delegate.selected_index();
 548        let new_index = if index + 1 == count { 0 } else { index + 1 };
 549        self.set_selected_index(new_index, Some(Direction::Down), true, window, cx);
 550        cx.notify();
 551    }
 552
 553    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 554        if self.delegate.should_dismiss() {
 555            self.delegate.dismissed(window, cx);
 556            cx.emit(DismissEvent);
 557        }
 558    }
 559
 560    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
 561        if self.pending_update_matches.is_some()
 562            && !self.delegate.finalize_update_matches(
 563                self.query(cx),
 564                Duration::from_millis(16),
 565                window,
 566                cx,
 567            )
 568        {
 569            self.confirm_on_update = Some(false)
 570        } else {
 571            self.pending_update_matches.take();
 572            self.do_confirm(false, window, cx);
 573        }
 574    }
 575
 576    fn secondary_confirm(
 577        &mut self,
 578        _: &menu::SecondaryConfirm,
 579        window: &mut Window,
 580        cx: &mut Context<Self>,
 581    ) {
 582        if self.pending_update_matches.is_some()
 583            && !self.delegate.finalize_update_matches(
 584                self.query(cx),
 585                Duration::from_millis(16),
 586                window,
 587                cx,
 588            )
 589        {
 590            self.confirm_on_update = Some(true)
 591        } else {
 592            self.do_confirm(true, window, cx);
 593        }
 594    }
 595
 596    fn confirm_input(&mut self, input: &ConfirmInput, window: &mut Window, cx: &mut Context<Self>) {
 597        self.delegate.confirm_input(input.secondary, window, cx);
 598    }
 599
 600    fn confirm_completion(
 601        &mut self,
 602        _: &ConfirmCompletion,
 603        window: &mut Window,
 604        cx: &mut Context<Self>,
 605    ) {
 606        if let Some(new_query) = self.delegate.confirm_completion(self.query(cx), window, cx) {
 607            self.set_query(&new_query, window, cx);
 608        } else {
 609            cx.propagate()
 610        }
 611    }
 612
 613    fn handle_click(
 614        &mut self,
 615        ix: usize,
 616        secondary: bool,
 617        window: &mut Window,
 618        cx: &mut Context<Self>,
 619    ) {
 620        cx.stop_propagation();
 621        window.prevent_default();
 622        if !self.delegate.can_select(ix, window, cx) {
 623            return;
 624        }
 625        self.set_selected_index(ix, None, false, window, cx);
 626        self.do_confirm(secondary, window, cx)
 627    }
 628
 629    fn do_confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Self>) {
 630        if let Some(update_query) = self.delegate.confirm_update_query(window, cx) {
 631            self.set_query(&update_query, window, cx);
 632            self.set_selected_index(0, Some(Direction::Down), false, window, cx);
 633        } else {
 634            self.delegate.confirm(secondary, window, cx)
 635        }
 636    }
 637
 638    fn on_input_editor_event(
 639        &mut self,
 640        event: &ErasedEditorEvent,
 641        window: &mut Window,
 642        cx: &mut Context<Self>,
 643    ) {
 644        let Head::Editor(editor) = &self.head else {
 645            panic!("unexpected call");
 646        };
 647        match event {
 648            ErasedEditorEvent::BufferEdited => {
 649                let query = editor.text(cx);
 650                self.update_matches(query, window, cx);
 651            }
 652            ErasedEditorEvent::Blurred => {
 653                if self.is_modal && window.is_window_active() {
 654                    self.cancel(&menu::Cancel, window, cx);
 655                }
 656            }
 657        }
 658    }
 659
 660    fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 661        let Head::Empty(_) = &self.head else {
 662            panic!("unexpected call");
 663        };
 664        if window.is_window_active() {
 665            self.cancel(&menu::Cancel, window, cx);
 666        }
 667    }
 668
 669    pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 670        match &self.head {
 671            Head::Editor(editor) => {
 672                let placeholder = self.delegate.placeholder_text(window, cx);
 673
 674                editor.set_placeholder_text(placeholder.as_ref(), window, cx);
 675                cx.notify();
 676            }
 677            Head::Empty(_) => {}
 678        }
 679    }
 680
 681    pub fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 682        let query = self.query(cx);
 683        self.update_matches(query, window, cx);
 684    }
 685
 686    pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
 687        let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx);
 688
 689        self.matches_updated(window, cx);
 690        // This struct ensures that we can synchronously drop the task returned by the
 691        // delegate's `update_matches` method and the task that the picker is spawning.
 692        // If we simply capture the delegate's task into the picker's task, when the picker's
 693        // task gets synchronously dropped, the delegate's task would keep running until
 694        // the picker's task has a chance of being scheduled, because dropping a task happens
 695        // asynchronously.
 696        self.pending_update_matches = Some(PendingUpdateMatches {
 697            delegate_update_matches: Some(delegate_pending_update_matches),
 698            _task: cx.spawn_in(window, async move |this, cx| {
 699                let delegate_pending_update_matches = this.update(cx, |this, _| {
 700                    this.pending_update_matches
 701                        .as_mut()
 702                        .unwrap()
 703                        .delegate_update_matches
 704                        .take()
 705                        .unwrap()
 706                })?;
 707                delegate_pending_update_matches.await;
 708                this.update_in(cx, |this, window, cx| {
 709                    this.matches_updated(window, cx);
 710                })
 711            }),
 712        });
 713    }
 714
 715    fn matches_updated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 716        if let ElementContainer::List(state) = &mut self.element_container {
 717            state.reset(self.delegate.match_count());
 718        }
 719
 720        let index = self.delegate.selected_index();
 721        self.scroll_to_item_index(index);
 722        self.pending_update_matches = None;
 723        if let Some(secondary) = self.confirm_on_update.take() {
 724            self.do_confirm(secondary, window, cx);
 725        }
 726        cx.notify();
 727    }
 728
 729    pub fn query(&self, cx: &App) -> String {
 730        match &self.head {
 731            Head::Editor(editor) => editor.text(cx),
 732            Head::Empty(_) => "".to_string(),
 733        }
 734    }
 735
 736    pub fn set_query(&self, query: &str, window: &mut Window, cx: &mut App) {
 737        if let Head::Editor(editor) = &self.head {
 738            editor.set_text(query, window, cx);
 739            editor.move_selection_to_end(window, cx);
 740        }
 741    }
 742
 743    fn scroll_to_item_index(&mut self, ix: usize) {
 744        match &mut self.element_container {
 745            ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
 746            ElementContainer::UniformList(scroll_handle) => {
 747                scroll_handle.scroll_to_item(ix, ScrollStrategy::Nearest)
 748            }
 749        }
 750    }
 751
 752    fn render_element(
 753        &self,
 754        window: &mut Window,
 755        cx: &mut Context<Self>,
 756        ix: usize,
 757    ) -> impl IntoElement + use<D> {
 758        let item_bounds = self.item_bounds.clone();
 759        let selectable = self.delegate.can_select(ix, window, cx);
 760
 761        div()
 762            .id(("item", ix))
 763            .when(selectable, |this| this.cursor_pointer())
 764            .child(
 765                canvas(
 766                    move |bounds, _window, _cx| {
 767                        item_bounds.borrow_mut().insert(ix, bounds);
 768                    },
 769                    |_bounds, _state, _window, _cx| {},
 770                )
 771                .size_full()
 772                .absolute()
 773                .top_0()
 774                .left_0(),
 775            )
 776            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
 777                this.handle_click(ix, event.modifiers().secondary(), window, cx)
 778            }))
 779            // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
 780            // and produces right mouse button events. This matches platforms norms
 781            // but means that UIs which depend on holding ctrl down (such as the tab
 782            // switcher) can't be clicked on. Hence, this handler.
 783            .on_mouse_up(
 784                MouseButton::Right,
 785                cx.listener(move |this, event: &MouseUpEvent, window, cx| {
 786                    // We specifically want to use the platform key here, as
 787                    // ctrl will already be held down for the tab switcher.
 788                    this.handle_click(ix, event.modifiers.platform, window, cx)
 789                }),
 790            )
 791            .children(self.delegate.render_match(
 792                ix,
 793                ix == self.delegate.selected_index(),
 794                window,
 795                cx,
 796            ))
 797            .when(
 798                self.delegate.separators_after_indices().contains(&ix),
 799                |picker| {
 800                    picker
 801                        .border_color(cx.theme().colors().border_variant)
 802                        .border_b_1()
 803                        .py(px(-1.0))
 804                },
 805            )
 806    }
 807
 808    fn render_element_container(&self, cx: &mut Context<Self>) -> impl IntoElement {
 809        let sizing_behavior = if self.max_height.is_some() {
 810            ListSizingBehavior::Infer
 811        } else {
 812            ListSizingBehavior::Auto
 813        };
 814
 815        match &self.element_container {
 816            ElementContainer::UniformList(scroll_handle) => uniform_list(
 817                "candidates",
 818                self.delegate.match_count(),
 819                cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
 820                    visible_range
 821                        .map(|ix| picker.render_element(window, cx, ix))
 822                        .collect()
 823                }),
 824            )
 825            .with_sizing_behavior(sizing_behavior)
 826            .when_some(self.widest_item, |el, widest_item| {
 827                el.with_width_from_item(Some(widest_item))
 828            })
 829            .flex_grow()
 830            .py_1()
 831            .track_scroll(&scroll_handle)
 832            .into_any_element(),
 833            ElementContainer::List(state) => list(
 834                state.clone(),
 835                cx.processor(|this, ix, window, cx| {
 836                    this.render_element(window, cx, ix).into_any_element()
 837                }),
 838            )
 839            .with_sizing_behavior(sizing_behavior)
 840            .flex_grow()
 841            .py_2()
 842            .into_any_element(),
 843        }
 844    }
 845
 846    #[cfg(any(test, feature = "test-support"))]
 847    pub fn logical_scroll_top_index(&self) -> usize {
 848        match &self.element_container {
 849            ElementContainer::List(state) => state.logical_scroll_top().item_ix,
 850            ElementContainer::UniformList(scroll_handle) => {
 851                scroll_handle.logical_scroll_top_index()
 852            }
 853        }
 854    }
 855}
 856
 857#[cfg(test)]
 858mod tests {
 859    use super::*;
 860    use gpui::TestAppContext;
 861    use std::cell::Cell;
 862
 863    struct TestDelegate {
 864        items: Vec<bool>,
 865        selected_index: usize,
 866        confirmed_index: Rc<Cell<Option<usize>>>,
 867    }
 868
 869    impl TestDelegate {
 870        fn new(items: Vec<bool>) -> Self {
 871            Self {
 872                items,
 873                selected_index: 0,
 874                confirmed_index: Rc::new(Cell::new(None)),
 875            }
 876        }
 877    }
 878
 879    impl PickerDelegate for TestDelegate {
 880        type ListItem = ui::ListItem;
 881
 882        fn match_count(&self) -> usize {
 883            self.items.len()
 884        }
 885
 886        fn selected_index(&self) -> usize {
 887            self.selected_index
 888        }
 889
 890        fn set_selected_index(
 891            &mut self,
 892            ix: usize,
 893            _window: &mut Window,
 894            _cx: &mut Context<Picker<Self>>,
 895        ) {
 896            self.selected_index = ix;
 897        }
 898
 899        fn can_select(
 900            &self,
 901            ix: usize,
 902            _window: &mut Window,
 903            _cx: &mut Context<Picker<Self>>,
 904        ) -> bool {
 905            self.items.get(ix).copied().unwrap_or(false)
 906        }
 907
 908        fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 909            "Test".into()
 910        }
 911
 912        fn update_matches(
 913            &mut self,
 914            _query: String,
 915            _window: &mut Window,
 916            _cx: &mut Context<Picker<Self>>,
 917        ) -> Task<()> {
 918            Task::ready(())
 919        }
 920
 921        fn confirm(
 922            &mut self,
 923            _secondary: bool,
 924            _window: &mut Window,
 925            _cx: &mut Context<Picker<Self>>,
 926        ) {
 927            self.confirmed_index.set(Some(self.selected_index));
 928        }
 929
 930        fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
 931
 932        fn render_match(
 933            &self,
 934            ix: usize,
 935            selected: bool,
 936            _window: &mut Window,
 937            _cx: &mut Context<Picker<Self>>,
 938        ) -> Option<Self::ListItem> {
 939            Some(
 940                ui::ListItem::new(ix)
 941                    .inset(true)
 942                    .toggle_state(selected)
 943                    .child(ui::Label::new(format!("Item {ix}"))),
 944            )
 945        }
 946    }
 947
 948    fn init_test(cx: &mut TestAppContext) {
 949        cx.update(|cx| {
 950            let store = settings::SettingsStore::test(cx);
 951            cx.set_global(store);
 952            theme::init(theme::LoadThemes::JustBase, cx);
 953            editor::init(cx);
 954        });
 955    }
 956
 957    #[gpui::test]
 958    async fn test_clicking_non_selectable_item_does_not_confirm(cx: &mut TestAppContext) {
 959        init_test(cx);
 960
 961        let confirmed_index = Rc::new(Cell::new(None));
 962        let (picker, cx) = cx.add_window_view(|window, cx| {
 963            let mut delegate = TestDelegate::new(vec![true, false, true]);
 964            delegate.confirmed_index = confirmed_index.clone();
 965            Picker::uniform_list(delegate, window, cx)
 966        });
 967
 968        picker.update(cx, |picker, _cx| {
 969            assert_eq!(picker.delegate.selected_index(), 0);
 970        });
 971
 972        picker.update_in(cx, |picker, window, cx| {
 973            picker.handle_click(1, false, window, cx);
 974        });
 975        assert!(
 976            confirmed_index.get().is_none(),
 977            "clicking a non-selectable item should not confirm"
 978        );
 979
 980        picker.update_in(cx, |picker, window, cx| {
 981            picker.handle_click(0, false, window, cx);
 982        });
 983        assert_eq!(
 984            confirmed_index.get(),
 985            Some(0),
 986            "clicking a selectable item should confirm"
 987        );
 988    }
 989
 990    #[gpui::test]
 991    async fn test_keyboard_navigation_skips_non_selectable_items(cx: &mut TestAppContext) {
 992        init_test(cx);
 993
 994        let (picker, cx) = cx.add_window_view(|window, cx| {
 995            Picker::uniform_list(TestDelegate::new(vec![true, false, true]), window, cx)
 996        });
 997
 998        picker.update(cx, |picker, _cx| {
 999            assert_eq!(picker.delegate.selected_index(), 0);
1000        });
1001
1002        picker.update_in(cx, |picker, window, cx| {
1003            picker.select_next(&menu::SelectNext, window, cx);
1004        });
1005        picker.update(cx, |picker, _cx| {
1006            assert_eq!(
1007                picker.delegate.selected_index(),
1008                2,
1009                "select_next should skip non-selectable item at index 1"
1010            );
1011        });
1012
1013        picker.update_in(cx, |picker, window, cx| {
1014            picker.select_previous(&menu::SelectPrevious, window, cx);
1015        });
1016        picker.update(cx, |picker, _cx| {
1017            assert_eq!(
1018                picker.delegate.selected_index(),
1019                0,
1020                "select_previous should skip non-selectable item at index 1"
1021            );
1022        });
1023    }
1024}
1025
1026impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
1027impl<D: PickerDelegate> ModalView for Picker<D> {}
1028
1029impl<D: PickerDelegate> Render for Picker<D> {
1030    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1031        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1032        let window_size = window.viewport_size();
1033        let rem_size = window.rem_size();
1034        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
1035
1036        let aside = self.delegate.documentation_aside(window, cx);
1037
1038        let editor_position = self.delegate.editor_position();
1039        let picker_bounds = self.picker_bounds.clone();
1040        let menu = v_flex()
1041            .key_context("Picker")
1042            .size_full()
1043            .when_some(self.width, |el, width| el.w(width))
1044            .overflow_hidden()
1045            .child(
1046                canvas(
1047                    move |bounds, _window, _cx| {
1048                        picker_bounds.set(Some(bounds));
1049                    },
1050                    |_bounds, _state, _window, _cx| {},
1051                )
1052                .size_full()
1053                .absolute()
1054                .top_0()
1055                .left_0(),
1056            )
1057            // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
1058            // as a part of a modal rather than the entire modal.
1059            //
1060            // We should revisit how the `Picker` is styled to make it more composable.
1061            .when(self.is_modal, |this| this.elevation_3(cx))
1062            .on_action(cx.listener(Self::select_next))
1063            .on_action(cx.listener(Self::select_previous))
1064            .on_action(cx.listener(Self::editor_move_down))
1065            .on_action(cx.listener(Self::editor_move_up))
1066            .on_action(cx.listener(Self::select_first))
1067            .on_action(cx.listener(Self::select_last))
1068            .on_action(cx.listener(Self::cancel))
1069            .on_action(cx.listener(Self::confirm))
1070            .on_action(cx.listener(Self::secondary_confirm))
1071            .on_action(cx.listener(Self::confirm_completion))
1072            .on_action(cx.listener(Self::confirm_input))
1073            .children(match &self.head {
1074                Head::Editor(editor) => {
1075                    if editor_position == PickerEditorPosition::Start {
1076                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
1077                    } else {
1078                        None
1079                    }
1080                }
1081                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
1082            })
1083            .when(self.delegate.match_count() > 0, |el| {
1084                el.child(
1085                    v_flex()
1086                        .id("element-container")
1087                        .relative()
1088                        .flex_grow()
1089                        .when_some(self.max_height, |div, max_h| div.max_h(max_h))
1090                        .overflow_hidden()
1091                        .children(self.delegate.render_header(window, cx))
1092                        .child(self.render_element_container(cx))
1093                        .when(self.show_scrollbar, |this| {
1094                            let base_scrollbar_config =
1095                                Scrollbars::new(ScrollAxes::Vertical).width_sm();
1096
1097                            this.map(|this| match &self.element_container {
1098                                ElementContainer::List(state) => this.custom_scrollbars(
1099                                    base_scrollbar_config.tracked_scroll_handle(state),
1100                                    window,
1101                                    cx,
1102                                ),
1103                                ElementContainer::UniformList(state) => this.custom_scrollbars(
1104                                    base_scrollbar_config.tracked_scroll_handle(state),
1105                                    window,
1106                                    cx,
1107                                ),
1108                            })
1109                        }),
1110                )
1111            })
1112            .when(self.delegate.match_count() == 0, |el| {
1113                el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
1114                    el.child(
1115                        v_flex().flex_grow().py_2().child(
1116                            ListItem::new("empty_state")
1117                                .inset(true)
1118                                .spacing(ListItemSpacing::Sparse)
1119                                .disabled(true)
1120                                .child(Label::new(text).color(Color::Muted)),
1121                        ),
1122                    )
1123                })
1124            })
1125            .children(self.delegate.render_footer(window, cx))
1126            .children(match &self.head {
1127                Head::Editor(editor) => {
1128                    if editor_position == PickerEditorPosition::End {
1129                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
1130                    } else {
1131                        None
1132                    }
1133                }
1134                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
1135            });
1136
1137        let Some(aside) = aside else {
1138            return menu;
1139        };
1140
1141        let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
1142            WithRemSize::new(ui_font_size)
1143                .occlude()
1144                .elevation_2(cx)
1145                .w_full()
1146                .p_2()
1147                .overflow_hidden()
1148                .when(is_wide_window, |this| this.max_w_96())
1149                .when(!is_wide_window, |this| this.max_w_48())
1150                .child((aside.render)(cx))
1151        };
1152
1153        if is_wide_window {
1154            let aside_index = self.delegate.documentation_aside_index();
1155            let picker_bounds = self.picker_bounds.get();
1156            let item_bounds =
1157                aside_index.and_then(|ix| self.item_bounds.borrow().get(&ix).copied());
1158
1159            let item_position = match (picker_bounds, item_bounds) {
1160                (Some(picker_bounds), Some(item_bounds)) => {
1161                    let relative_top = item_bounds.origin.y - picker_bounds.origin.y;
1162                    let height = item_bounds.size.height;
1163                    Some((relative_top, height))
1164                }
1165                _ => None,
1166            };
1167
1168            div()
1169                .relative()
1170                .child(menu)
1171                // Only render the aside once we have bounds to avoid flicker
1172                .when_some(item_position, |this, (top, height)| {
1173                    this.child(
1174                        h_flex()
1175                            .absolute()
1176                            .when(aside.side == DocumentationSide::Left, |el| {
1177                                el.right_full().mr_1()
1178                            })
1179                            .when(aside.side == DocumentationSide::Right, |el| {
1180                                el.left_full().ml_1()
1181                            })
1182                            .top(top)
1183                            .h(height)
1184                            .child(render_aside(aside, cx)),
1185                    )
1186                })
1187        } else {
1188            v_flex()
1189                .w_full()
1190                .gap_1()
1191                .justify_end()
1192                .child(render_aside(aside, cx))
1193                .child(menu)
1194        }
1195    }
1196}