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        &mut 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        self.set_selected_index(ix, None, false, window, cx);
 623        self.do_confirm(secondary, window, cx)
 624    }
 625
 626    fn do_confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Self>) {
 627        if let Some(update_query) = self.delegate.confirm_update_query(window, cx) {
 628            self.set_query(&update_query, window, cx);
 629            self.set_selected_index(0, Some(Direction::Down), false, window, cx);
 630        } else {
 631            self.delegate.confirm(secondary, window, cx)
 632        }
 633    }
 634
 635    fn on_input_editor_event(
 636        &mut self,
 637        event: &ErasedEditorEvent,
 638        window: &mut Window,
 639        cx: &mut Context<Self>,
 640    ) {
 641        let Head::Editor(editor) = &self.head else {
 642            panic!("unexpected call");
 643        };
 644        match event {
 645            ErasedEditorEvent::BufferEdited => {
 646                let query = editor.text(cx);
 647                self.update_matches(query, window, cx);
 648            }
 649            ErasedEditorEvent::Blurred => {
 650                if self.is_modal && window.is_window_active() {
 651                    self.cancel(&menu::Cancel, window, cx);
 652                }
 653            }
 654        }
 655    }
 656
 657    fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 658        let Head::Empty(_) = &self.head else {
 659            panic!("unexpected call");
 660        };
 661        if window.is_window_active() {
 662            self.cancel(&menu::Cancel, window, cx);
 663        }
 664    }
 665
 666    pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 667        match &self.head {
 668            Head::Editor(editor) => {
 669                let placeholder = self.delegate.placeholder_text(window, cx);
 670
 671                editor.set_placeholder_text(placeholder.as_ref(), window, cx);
 672                cx.notify();
 673            }
 674            Head::Empty(_) => {}
 675        }
 676    }
 677
 678    pub fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 679        let query = self.query(cx);
 680        self.update_matches(query, window, cx);
 681    }
 682
 683    pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
 684        let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx);
 685
 686        self.matches_updated(window, cx);
 687        // This struct ensures that we can synchronously drop the task returned by the
 688        // delegate's `update_matches` method and the task that the picker is spawning.
 689        // If we simply capture the delegate's task into the picker's task, when the picker's
 690        // task gets synchronously dropped, the delegate's task would keep running until
 691        // the picker's task has a chance of being scheduled, because dropping a task happens
 692        // asynchronously.
 693        self.pending_update_matches = Some(PendingUpdateMatches {
 694            delegate_update_matches: Some(delegate_pending_update_matches),
 695            _task: cx.spawn_in(window, async move |this, cx| {
 696                let delegate_pending_update_matches = this.update(cx, |this, _| {
 697                    this.pending_update_matches
 698                        .as_mut()
 699                        .unwrap()
 700                        .delegate_update_matches
 701                        .take()
 702                        .unwrap()
 703                })?;
 704                delegate_pending_update_matches.await;
 705                this.update_in(cx, |this, window, cx| {
 706                    this.matches_updated(window, cx);
 707                })
 708            }),
 709        });
 710    }
 711
 712    fn matches_updated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 713        if let ElementContainer::List(state) = &mut self.element_container {
 714            state.reset(self.delegate.match_count());
 715        }
 716
 717        let index = self.delegate.selected_index();
 718        self.scroll_to_item_index(index);
 719        self.pending_update_matches = None;
 720        if let Some(secondary) = self.confirm_on_update.take() {
 721            self.do_confirm(secondary, window, cx);
 722        }
 723        cx.notify();
 724    }
 725
 726    pub fn query(&self, cx: &App) -> String {
 727        match &self.head {
 728            Head::Editor(editor) => editor.text(cx),
 729            Head::Empty(_) => "".to_string(),
 730        }
 731    }
 732
 733    pub fn set_query(&self, query: &str, window: &mut Window, cx: &mut App) {
 734        if let Head::Editor(editor) = &self.head {
 735            editor.set_text(query, window, cx);
 736            editor.move_selection_to_end(window, cx);
 737        }
 738    }
 739
 740    fn scroll_to_item_index(&mut self, ix: usize) {
 741        match &mut self.element_container {
 742            ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
 743            ElementContainer::UniformList(scroll_handle) => {
 744                scroll_handle.scroll_to_item(ix, ScrollStrategy::Nearest)
 745            }
 746        }
 747    }
 748
 749    fn render_element(
 750        &self,
 751        window: &mut Window,
 752        cx: &mut Context<Self>,
 753        ix: usize,
 754    ) -> impl IntoElement + use<D> {
 755        let item_bounds = self.item_bounds.clone();
 756
 757        div()
 758            .id(("item", ix))
 759            .cursor_pointer()
 760            .child(
 761                canvas(
 762                    move |bounds, _window, _cx| {
 763                        item_bounds.borrow_mut().insert(ix, bounds);
 764                    },
 765                    |_bounds, _state, _window, _cx| {},
 766                )
 767                .size_full()
 768                .absolute()
 769                .top_0()
 770                .left_0(),
 771            )
 772            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
 773                this.handle_click(ix, event.modifiers().secondary(), window, cx)
 774            }))
 775            // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
 776            // and produces right mouse button events. This matches platforms norms
 777            // but means that UIs which depend on holding ctrl down (such as the tab
 778            // switcher) can't be clicked on. Hence, this handler.
 779            .on_mouse_up(
 780                MouseButton::Right,
 781                cx.listener(move |this, event: &MouseUpEvent, window, cx| {
 782                    // We specifically want to use the platform key here, as
 783                    // ctrl will already be held down for the tab switcher.
 784                    this.handle_click(ix, event.modifiers.platform, window, cx)
 785                }),
 786            )
 787            .children(self.delegate.render_match(
 788                ix,
 789                ix == self.delegate.selected_index(),
 790                window,
 791                cx,
 792            ))
 793            .when(
 794                self.delegate.separators_after_indices().contains(&ix),
 795                |picker| {
 796                    picker
 797                        .border_color(cx.theme().colors().border_variant)
 798                        .border_b_1()
 799                        .py(px(-1.0))
 800                },
 801            )
 802    }
 803
 804    fn render_element_container(&self, cx: &mut Context<Self>) -> impl IntoElement {
 805        let sizing_behavior = if self.max_height.is_some() {
 806            ListSizingBehavior::Infer
 807        } else {
 808            ListSizingBehavior::Auto
 809        };
 810
 811        match &self.element_container {
 812            ElementContainer::UniformList(scroll_handle) => uniform_list(
 813                "candidates",
 814                self.delegate.match_count(),
 815                cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
 816                    visible_range
 817                        .map(|ix| picker.render_element(window, cx, ix))
 818                        .collect()
 819                }),
 820            )
 821            .with_sizing_behavior(sizing_behavior)
 822            .when_some(self.widest_item, |el, widest_item| {
 823                el.with_width_from_item(Some(widest_item))
 824            })
 825            .flex_grow()
 826            .py_1()
 827            .track_scroll(&scroll_handle)
 828            .into_any_element(),
 829            ElementContainer::List(state) => list(
 830                state.clone(),
 831                cx.processor(|this, ix, window, cx| {
 832                    this.render_element(window, cx, ix).into_any_element()
 833                }),
 834            )
 835            .with_sizing_behavior(sizing_behavior)
 836            .flex_grow()
 837            .py_2()
 838            .into_any_element(),
 839        }
 840    }
 841
 842    #[cfg(any(test, feature = "test-support"))]
 843    pub fn logical_scroll_top_index(&self) -> usize {
 844        match &self.element_container {
 845            ElementContainer::List(state) => state.logical_scroll_top().item_ix,
 846            ElementContainer::UniformList(scroll_handle) => {
 847                scroll_handle.logical_scroll_top_index()
 848            }
 849        }
 850    }
 851}
 852
 853impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
 854impl<D: PickerDelegate> ModalView for Picker<D> {}
 855
 856impl<D: PickerDelegate> Render for Picker<D> {
 857    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 858        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
 859        let window_size = window.viewport_size();
 860        let rem_size = window.rem_size();
 861        let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0;
 862
 863        let aside = self.delegate.documentation_aside(window, cx);
 864
 865        let editor_position = self.delegate.editor_position();
 866        let picker_bounds = self.picker_bounds.clone();
 867        let menu = v_flex()
 868            .key_context("Picker")
 869            .size_full()
 870            .when_some(self.width, |el, width| el.w(width))
 871            .overflow_hidden()
 872            .child(
 873                canvas(
 874                    move |bounds, _window, _cx| {
 875                        picker_bounds.set(Some(bounds));
 876                    },
 877                    |_bounds, _state, _window, _cx| {},
 878                )
 879                .size_full()
 880                .absolute()
 881                .top_0()
 882                .left_0(),
 883            )
 884            // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
 885            // as a part of a modal rather than the entire modal.
 886            //
 887            // We should revisit how the `Picker` is styled to make it more composable.
 888            .when(self.is_modal, |this| this.elevation_3(cx))
 889            .on_action(cx.listener(Self::select_next))
 890            .on_action(cx.listener(Self::select_previous))
 891            .on_action(cx.listener(Self::editor_move_down))
 892            .on_action(cx.listener(Self::editor_move_up))
 893            .on_action(cx.listener(Self::select_first))
 894            .on_action(cx.listener(Self::select_last))
 895            .on_action(cx.listener(Self::cancel))
 896            .on_action(cx.listener(Self::confirm))
 897            .on_action(cx.listener(Self::secondary_confirm))
 898            .on_action(cx.listener(Self::confirm_completion))
 899            .on_action(cx.listener(Self::confirm_input))
 900            .children(match &self.head {
 901                Head::Editor(editor) => {
 902                    if editor_position == PickerEditorPosition::Start {
 903                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
 904                    } else {
 905                        None
 906                    }
 907                }
 908                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
 909            })
 910            .when(self.delegate.match_count() > 0, |el| {
 911                el.child(
 912                    v_flex()
 913                        .id("element-container")
 914                        .relative()
 915                        .flex_grow()
 916                        .when_some(self.max_height, |div, max_h| div.max_h(max_h))
 917                        .overflow_hidden()
 918                        .children(self.delegate.render_header(window, cx))
 919                        .child(self.render_element_container(cx))
 920                        .when(self.show_scrollbar, |this| {
 921                            let base_scrollbar_config =
 922                                Scrollbars::new(ScrollAxes::Vertical).width_sm();
 923
 924                            this.map(|this| match &self.element_container {
 925                                ElementContainer::List(state) => this.custom_scrollbars(
 926                                    base_scrollbar_config.tracked_scroll_handle(state),
 927                                    window,
 928                                    cx,
 929                                ),
 930                                ElementContainer::UniformList(state) => this.custom_scrollbars(
 931                                    base_scrollbar_config.tracked_scroll_handle(state),
 932                                    window,
 933                                    cx,
 934                                ),
 935                            })
 936                        }),
 937                )
 938            })
 939            .when(self.delegate.match_count() == 0, |el| {
 940                el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
 941                    el.child(
 942                        v_flex().flex_grow().py_2().child(
 943                            ListItem::new("empty_state")
 944                                .inset(true)
 945                                .spacing(ListItemSpacing::Sparse)
 946                                .disabled(true)
 947                                .child(Label::new(text).color(Color::Muted)),
 948                        ),
 949                    )
 950                })
 951            })
 952            .children(self.delegate.render_footer(window, cx))
 953            .children(match &self.head {
 954                Head::Editor(editor) => {
 955                    if editor_position == PickerEditorPosition::End {
 956                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
 957                    } else {
 958                        None
 959                    }
 960                }
 961                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
 962            });
 963
 964        let Some(aside) = aside else {
 965            return menu;
 966        };
 967
 968        let render_aside = |aside: DocumentationAside, cx: &mut Context<Self>| {
 969            WithRemSize::new(ui_font_size)
 970                .occlude()
 971                .elevation_2(cx)
 972                .w_full()
 973                .p_2()
 974                .overflow_hidden()
 975                .when(is_wide_window, |this| this.max_w_96())
 976                .when(!is_wide_window, |this| this.max_w_48())
 977                .child((aside.render)(cx))
 978        };
 979
 980        if is_wide_window {
 981            let aside_index = self.delegate.documentation_aside_index();
 982            let picker_bounds = self.picker_bounds.get();
 983            let item_bounds =
 984                aside_index.and_then(|ix| self.item_bounds.borrow().get(&ix).copied());
 985
 986            let item_position = match (picker_bounds, item_bounds) {
 987                (Some(picker_bounds), Some(item_bounds)) => {
 988                    let relative_top = item_bounds.origin.y - picker_bounds.origin.y;
 989                    let height = item_bounds.size.height;
 990                    Some((relative_top, height))
 991                }
 992                _ => None,
 993            };
 994
 995            div()
 996                .relative()
 997                .child(menu)
 998                // Only render the aside once we have bounds to avoid flicker
 999                .when_some(item_position, |this, (top, height)| {
1000                    this.child(
1001                        h_flex()
1002                            .absolute()
1003                            .when(aside.side == DocumentationSide::Left, |el| {
1004                                el.right_full().mr_1()
1005                            })
1006                            .when(aside.side == DocumentationSide::Right, |el| {
1007                                el.left_full().ml_1()
1008                            })
1009                            .top(top)
1010                            .h(height)
1011                            .child(render_aside(aside, cx)),
1012                    )
1013                })
1014        } else {
1015            v_flex()
1016                .w_full()
1017                .gap_1()
1018                .justify_end()
1019                .child(render_aside(aside, cx))
1020                .child(menu)
1021        }
1022    }
1023}