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            .on_hover(cx.listener(move |this, hovered: &bool, window, cx| {
 792                if *hovered {
 793                    this.set_selected_index(ix, None, false, window, cx);
 794                    cx.notify();
 795                }
 796            }))
 797            .children(self.delegate.render_match(
 798                ix,
 799                ix == self.delegate.selected_index(),
 800                window,
 801                cx,
 802            ))
 803            .when(
 804                self.delegate.separators_after_indices().contains(&ix),
 805                |picker| {
 806                    picker
 807                        .border_color(cx.theme().colors().border_variant)
 808                        .border_b_1()
 809                        .py(px(-1.0))
 810                },
 811            )
 812    }
 813
 814    fn render_element_container(&self, cx: &mut Context<Self>) -> impl IntoElement {
 815        let sizing_behavior = if self.max_height.is_some() {
 816            ListSizingBehavior::Infer
 817        } else {
 818            ListSizingBehavior::Auto
 819        };
 820
 821        match &self.element_container {
 822            ElementContainer::UniformList(scroll_handle) => uniform_list(
 823                "candidates",
 824                self.delegate.match_count(),
 825                cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
 826                    visible_range
 827                        .map(|ix| picker.render_element(window, cx, ix))
 828                        .collect()
 829                }),
 830            )
 831            .with_sizing_behavior(sizing_behavior)
 832            .when_some(self.widest_item, |el, widest_item| {
 833                el.with_width_from_item(Some(widest_item))
 834            })
 835            .flex_grow()
 836            .py_1()
 837            .track_scroll(&scroll_handle)
 838            .into_any_element(),
 839            ElementContainer::List(state) => list(
 840                state.clone(),
 841                cx.processor(|this, ix, window, cx| {
 842                    this.render_element(window, cx, ix).into_any_element()
 843                }),
 844            )
 845            .with_sizing_behavior(sizing_behavior)
 846            .flex_grow()
 847            .py_2()
 848            .into_any_element(),
 849        }
 850    }
 851
 852    #[cfg(any(test, feature = "test-support"))]
 853    pub fn logical_scroll_top_index(&self) -> usize {
 854        match &self.element_container {
 855            ElementContainer::List(state) => state.logical_scroll_top().item_ix,
 856            ElementContainer::UniformList(scroll_handle) => {
 857                scroll_handle.logical_scroll_top_index()
 858            }
 859        }
 860    }
 861}
 862
 863#[cfg(test)]
 864mod tests {
 865    use super::*;
 866    use gpui::TestAppContext;
 867    use std::cell::Cell;
 868
 869    struct TestDelegate {
 870        items: Vec<bool>,
 871        selected_index: usize,
 872        confirmed_index: Rc<Cell<Option<usize>>>,
 873    }
 874
 875    impl TestDelegate {
 876        fn new(items: Vec<bool>) -> Self {
 877            Self {
 878                items,
 879                selected_index: 0,
 880                confirmed_index: Rc::new(Cell::new(None)),
 881            }
 882        }
 883    }
 884
 885    impl PickerDelegate for TestDelegate {
 886        type ListItem = ui::ListItem;
 887
 888        fn match_count(&self) -> usize {
 889            self.items.len()
 890        }
 891
 892        fn selected_index(&self) -> usize {
 893            self.selected_index
 894        }
 895
 896        fn set_selected_index(
 897            &mut self,
 898            ix: usize,
 899            _window: &mut Window,
 900            _cx: &mut Context<Picker<Self>>,
 901        ) {
 902            self.selected_index = ix;
 903        }
 904
 905        fn can_select(
 906            &self,
 907            ix: usize,
 908            _window: &mut Window,
 909            _cx: &mut Context<Picker<Self>>,
 910        ) -> bool {
 911            self.items.get(ix).copied().unwrap_or(false)
 912        }
 913
 914        fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 915            "Test".into()
 916        }
 917
 918        fn update_matches(
 919            &mut self,
 920            _query: String,
 921            _window: &mut Window,
 922            _cx: &mut Context<Picker<Self>>,
 923        ) -> Task<()> {
 924            Task::ready(())
 925        }
 926
 927        fn confirm(
 928            &mut self,
 929            _secondary: bool,
 930            _window: &mut Window,
 931            _cx: &mut Context<Picker<Self>>,
 932        ) {
 933            self.confirmed_index.set(Some(self.selected_index));
 934        }
 935
 936        fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
 937
 938        fn render_match(
 939            &self,
 940            ix: usize,
 941            selected: bool,
 942            _window: &mut Window,
 943            _cx: &mut Context<Picker<Self>>,
 944        ) -> Option<Self::ListItem> {
 945            Some(
 946                ui::ListItem::new(ix)
 947                    .inset(true)
 948                    .toggle_state(selected)
 949                    .child(ui::Label::new(format!("Item {ix}"))),
 950            )
 951        }
 952    }
 953
 954    fn init_test(cx: &mut TestAppContext) {
 955        cx.update(|cx| {
 956            let store = settings::SettingsStore::test(cx);
 957            cx.set_global(store);
 958            theme::init(theme::LoadThemes::JustBase, cx);
 959            editor::init(cx);
 960        });
 961    }
 962
 963    #[gpui::test]
 964    async fn test_clicking_non_selectable_item_does_not_confirm(cx: &mut TestAppContext) {
 965        init_test(cx);
 966
 967        let confirmed_index = Rc::new(Cell::new(None));
 968        let (picker, cx) = cx.add_window_view(|window, cx| {
 969            let mut delegate = TestDelegate::new(vec![true, false, true]);
 970            delegate.confirmed_index = confirmed_index.clone();
 971            Picker::uniform_list(delegate, window, cx)
 972        });
 973
 974        picker.update(cx, |picker, _cx| {
 975            assert_eq!(picker.delegate.selected_index(), 0);
 976        });
 977
 978        picker.update_in(cx, |picker, window, cx| {
 979            picker.handle_click(1, false, window, cx);
 980        });
 981        assert!(
 982            confirmed_index.get().is_none(),
 983            "clicking a non-selectable item should not confirm"
 984        );
 985
 986        picker.update_in(cx, |picker, window, cx| {
 987            picker.handle_click(0, false, window, cx);
 988        });
 989        assert_eq!(
 990            confirmed_index.get(),
 991            Some(0),
 992            "clicking a selectable item should confirm"
 993        );
 994    }
 995
 996    #[gpui::test]
 997    async fn test_keyboard_navigation_skips_non_selectable_items(cx: &mut TestAppContext) {
 998        init_test(cx);
 999
1000        let (picker, cx) = cx.add_window_view(|window, cx| {
1001            Picker::uniform_list(TestDelegate::new(vec![true, false, true]), window, cx)
1002        });
1003
1004        picker.update(cx, |picker, _cx| {
1005            assert_eq!(picker.delegate.selected_index(), 0);
1006        });
1007
1008        picker.update_in(cx, |picker, window, cx| {
1009            picker.select_next(&menu::SelectNext, window, cx);
1010        });
1011        picker.update(cx, |picker, _cx| {
1012            assert_eq!(
1013                picker.delegate.selected_index(),
1014                2,
1015                "select_next should skip non-selectable item at index 1"
1016            );
1017        });
1018
1019        picker.update_in(cx, |picker, window, cx| {
1020            picker.select_previous(&menu::SelectPrevious, window, cx);
1021        });
1022        picker.update(cx, |picker, _cx| {
1023            assert_eq!(
1024                picker.delegate.selected_index(),
1025                0,
1026                "select_previous should skip non-selectable item at index 1"
1027            );
1028        });
1029    }
1030}
1031
1032impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
1033impl<D: PickerDelegate> ModalView for Picker<D> {}
1034
1035impl<D: PickerDelegate> Render for Picker<D> {
1036    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1037        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
1038        let window_size = window.viewport_size();
1039        let rem_size = window.rem_size();
1040        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
1041
1042        let aside = self.delegate.documentation_aside(window, cx);
1043
1044        let editor_position = self.delegate.editor_position();
1045        let picker_bounds = self.picker_bounds.clone();
1046        let menu = v_flex()
1047            .key_context("Picker")
1048            .size_full()
1049            .when_some(self.width, |el, width| el.w(width))
1050            .overflow_hidden()
1051            .child(
1052                canvas(
1053                    move |bounds, _window, _cx| {
1054                        picker_bounds.set(Some(bounds));
1055                    },
1056                    |_bounds, _state, _window, _cx| {},
1057                )
1058                .size_full()
1059                .absolute()
1060                .top_0()
1061                .left_0(),
1062            )
1063            // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
1064            // as a part of a modal rather than the entire modal.
1065            //
1066            // We should revisit how the `Picker` is styled to make it more composable.
1067            .when(self.is_modal, |this| this.elevation_3(cx))
1068            .on_action(cx.listener(Self::select_next))
1069            .on_action(cx.listener(Self::select_previous))
1070            .on_action(cx.listener(Self::editor_move_down))
1071            .on_action(cx.listener(Self::editor_move_up))
1072            .on_action(cx.listener(Self::select_first))
1073            .on_action(cx.listener(Self::select_last))
1074            .on_action(cx.listener(Self::cancel))
1075            .on_action(cx.listener(Self::confirm))
1076            .on_action(cx.listener(Self::secondary_confirm))
1077            .on_action(cx.listener(Self::confirm_completion))
1078            .on_action(cx.listener(Self::confirm_input))
1079            .children(match &self.head {
1080                Head::Editor(editor) => {
1081                    if editor_position == PickerEditorPosition::Start {
1082                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
1083                    } else {
1084                        None
1085                    }
1086                }
1087                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
1088            })
1089            .when(self.delegate.match_count() > 0, |el| {
1090                el.child(
1091                    v_flex()
1092                        .id("element-container")
1093                        .relative()
1094                        .flex_grow()
1095                        .when_some(self.max_height, |div, max_h| div.max_h(max_h))
1096                        .overflow_hidden()
1097                        .children(self.delegate.render_header(window, cx))
1098                        .child(self.render_element_container(cx))
1099                        .when(self.show_scrollbar, |this| {
1100                            let base_scrollbar_config =
1101                                Scrollbars::new(ScrollAxes::Vertical).width_sm();
1102
1103                            this.map(|this| match &self.element_container {
1104                                ElementContainer::List(state) => this.custom_scrollbars(
1105                                    base_scrollbar_config.tracked_scroll_handle(state),
1106                                    window,
1107                                    cx,
1108                                ),
1109                                ElementContainer::UniformList(state) => this.custom_scrollbars(
1110                                    base_scrollbar_config.tracked_scroll_handle(state),
1111                                    window,
1112                                    cx,
1113                                ),
1114                            })
1115                        }),
1116                )
1117            })
1118            .when(self.delegate.match_count() == 0, |el| {
1119                el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
1120                    el.child(
1121                        v_flex().flex_grow().py_2().child(
1122                            ListItem::new("empty_state")
1123                                .inset(true)
1124                                .spacing(ListItemSpacing::Sparse)
1125                                .disabled(true)
1126                                .child(Label::new(text).color(Color::Muted)),
1127                        ),
1128                    )
1129                })
1130            })
1131            .children(self.delegate.render_footer(window, cx))
1132            .children(match &self.head {
1133                Head::Editor(editor) => {
1134                    if editor_position == PickerEditorPosition::End {
1135                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
1136                    } else {
1137                        None
1138                    }
1139                }
1140                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
1141            });
1142
1143        let Some(aside) = aside else {
1144            return menu;
1145        };
1146
1147        let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
1148            WithRemSize::new(ui_font_size)
1149                .occlude()
1150                .elevation_2(cx)
1151                .w_full()
1152                .p_2()
1153                .overflow_hidden()
1154                .when(is_wide_window, |this| this.max_w_96())
1155                .when(!is_wide_window, |this| this.max_w_48())
1156                .child((aside.render)(cx))
1157        };
1158
1159        if is_wide_window {
1160            let aside_index = self.delegate.documentation_aside_index();
1161            let picker_bounds = self.picker_bounds.get();
1162            let item_bounds =
1163                aside_index.and_then(|ix| self.item_bounds.borrow().get(&ix).copied());
1164
1165            let item_position = match (picker_bounds, item_bounds) {
1166                (Some(picker_bounds), Some(item_bounds)) => {
1167                    let relative_top = item_bounds.origin.y - picker_bounds.origin.y;
1168                    let height = item_bounds.size.height;
1169                    Some((relative_top, height))
1170                }
1171                _ => None,
1172            };
1173
1174            div()
1175                .relative()
1176                .child(menu)
1177                // Only render the aside once we have bounds to avoid flicker
1178                .when_some(item_position, |this, (top, height)| {
1179                    this.child(
1180                        h_flex()
1181                            .absolute()
1182                            .when(aside.side == DocumentationSide::Left, |el| {
1183                                el.right_full().mr_1()
1184                            })
1185                            .when(aside.side == DocumentationSide::Right, |el| {
1186                                el.left_full().ml_1()
1187                            })
1188                            .top(top)
1189                            .h(height)
1190                            .child(render_aside(aside, cx)),
1191                    )
1192                })
1193        } else {
1194            v_flex()
1195                .w_full()
1196                .gap_1()
1197                .justify_end()
1198                .child(render_aside(aside, cx))
1199                .child(menu)
1200        }
1201    }
1202}