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