Skip to main content

gpui_component/command/
state.rs

1use std::rc::Rc;
2
3use gpui::{
4    AbsoluteLength, AnyElement, App, AppContext as _, AvailableSpace, Context, Entity, FocusHandle,
5    Focusable, FontFallbacks, FontFeatures, FontStyle, FontWeight, InteractiveElement, IntoElement,
6    KeyBinding, ListSizingBehavior, ParentElement, Pixels, Render, Role, ScrollStrategy,
7    SharedString, Size, StatefulInteractiveElement as _, StyleRefinement, Styled, Subscription,
8    TextOverflow, WhiteSpace, Window, div, prelude::FluentBuilder as _, px, size,
9};
10use rust_i18n::t;
11
12use crate::{
13    ActiveTheme as _, ElementExt as _, Icon, IconName, IndexPath, StyledExt as _,
14    VirtualListScrollHandle,
15    actions::{Cancel, Confirm, SelectDown, SelectUp},
16    command::{
17        command::CommandOptions,
18        item::{CommandEntry, CommandItem},
19    },
20    h_flex,
21    input::{Input, InputEvent, InputState},
22    kbd::Kbd,
23    scroll::Scrollbar,
24    v_flex, v_virtual_list,
25};
26
27pub(crate) const CONTEXT: &str = "Command";
28
29/// The row a separator occupies: a one-pixel rule with a little air on
30/// either side. Fixed, so that only the item and heading rows need measuring.
31const SEPARATOR_ROW_HEIGHT: f32 = 9.;
32
33pub(crate) type OnQuery = dyn Fn(&str, &mut Window, &mut App);
34pub(crate) type OnIndex = dyn Fn(IndexPath, &mut Window, &mut App);
35pub(crate) type OnCancel = dyn Fn(&mut Window, &mut App);
36
37pub(crate) struct CommandModel {
38    pub(crate) entries: Vec<CommandEntry>,
39    pub(crate) searchable: bool,
40    pub(crate) filterable: bool,
41    pub(crate) on_query: Option<Rc<OnQuery>>,
42    pub(crate) on_select: Option<Rc<OnIndex>>,
43    pub(crate) on_confirm: Option<Rc<OnIndex>>,
44    pub(crate) on_cancel: Option<Rc<OnCancel>>,
45}
46
47impl Default for CommandModel {
48    fn default() -> Self {
49        Self {
50            entries: Vec::new(),
51            searchable: true,
52            filterable: true,
53            on_query: None,
54            on_select: None,
55            on_confirm: None,
56            on_cancel: None,
57        }
58    }
59}
60
61pub(crate) fn init(cx: &mut App) {
62    let context: Option<&str> = Some(CONTEXT);
63    cx.bind_keys([
64        KeyBinding::new("escape", Cancel, context),
65        KeyBinding::new("enter", Confirm { secondary: false }, context),
66        KeyBinding::new("up", SelectUp, context),
67        KeyBinding::new("down", SelectDown, context),
68    ]);
69}
70
71/// One rendered line of the list.
72///
73/// Groups are flattened into headings and items so the list is a single
74/// sequence of rows, which is what the virtual list scrolls over.
75#[derive(Clone, PartialEq)]
76enum CommandRow {
77    Heading(SharedString),
78    /// Holds the index into [`CommandState::matched`].
79    Item(usize),
80    Separator,
81}
82
83#[derive(Clone, PartialEq)]
84struct TextShapeKey {
85    font_family: SharedString,
86    font_features: FontFeatures,
87    font_fallbacks: Option<FontFallbacks>,
88    font_size: AbsoluteLength,
89    font_weight: FontWeight,
90    font_style: FontStyle,
91    white_space: WhiteSpace,
92    text_overflow: Option<TextOverflow>,
93    line_clamp: Option<usize>,
94}
95
96#[derive(Clone, PartialEq)]
97struct ListMeasurementKey {
98    content_width: Pixels,
99    rem_size: Pixels,
100    line_height: Pixels,
101    text_shape: TextShapeKey,
102}
103
104/// An item that survived the current query, and where it landed.
105#[derive(Clone)]
106struct MatchedItem {
107    entry_ix: usize,
108    item_ix: usize,
109    index_path: IndexPath,
110    row_ix: usize,
111    disabled: bool,
112}
113
114/// The interaction state of a [`crate::command::Command`] palette: its query,
115/// focus, scrolling, and highlighted command.
116pub struct CommandState {
117    focus_handle: FocusHandle,
118    query_input: Entity<InputState>,
119    scroll_handle: VirtualListScrollHandle,
120    model: CommandModel,
121    rows: Vec<CommandRow>,
122    row_sizes: Rc<Vec<Size<Pixels>>>,
123    list_measurement_key: Option<ListMeasurementKey>,
124    needs_measure: bool,
125    matched: Vec<MatchedItem>,
126    selected_index: Option<usize>,
127    preserve_no_selection: bool,
128    loading: bool,
129    pending_scroll: Option<usize>,
130    /// The placeholder last written to the query input, so that `render` only
131    /// writes when it changed — `set_placeholder` notifies, and an
132    /// unconditional notify from `render` would redraw every frame.
133    applied_placeholder: SharedString,
134    applied_query: SharedString,
135    pub(crate) options: CommandOptions,
136    _subscriptions: Vec<Subscription>,
137}
138
139impl CommandState {
140    /// Create an empty palette.
141    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
142        let query_input = cx.new(|cx| InputState::new(window, cx));
143
144        let _subscriptions =
145            vec![cx.subscribe_in(&query_input, window, Self::on_query_input_event)];
146
147        Self {
148            focus_handle: cx.focus_handle(),
149            query_input,
150            scroll_handle: VirtualListScrollHandle::new(),
151            model: CommandModel::default(),
152            rows: Vec::new(),
153            row_sizes: Rc::new(Vec::new()),
154            list_measurement_key: None,
155            needs_measure: true,
156            matched: Vec::new(),
157            selected_index: None,
158            preserve_no_selection: false,
159            loading: false,
160            pending_scroll: None,
161            applied_placeholder: SharedString::default(),
162            applied_query: SharedString::default(),
163            options: CommandOptions::default(),
164            _subscriptions,
165        }
166    }
167
168    pub(crate) fn install_model(&mut self, model: CommandModel, cx: &mut Context<Self>) {
169        let selected_index_path = self.selected_index();
170        self.model = model;
171        self.update_matches(cx);
172
173        let preserved_selection = selected_index_path.and_then(|selected_index_path| {
174            self.matched
175                .iter()
176                .enumerate()
177                .find_map(|(matched_ix, matched)| {
178                    (!matched.disabled && matched.index_path == selected_index_path)
179                        .then_some(matched_ix)
180                })
181        });
182
183        if let Some(matched_ix) = preserved_selection {
184            // Preserving the selection is not a navigation: the model
185            // reinstalls on every host re-render, so scrolling here would
186            // move the list one frame after a hover selection.
187            self.selected_index = Some(matched_ix);
188            self.preserve_no_selection = false;
189        } else if self.preserve_no_selection {
190            self.selected_index = None;
191            self.pending_scroll = None;
192        } else {
193            self.reset_selection();
194        }
195
196        self.needs_measure = true;
197    }
198
199    /// The current search query.
200    pub fn query(&self, cx: &App) -> SharedString {
201        self.query_input.read(cx).value()
202    }
203
204    /// Replace the search query, as if it had been typed.
205    ///
206    /// The input suppresses its own change event for a programmatic write, so
207    /// the re-filter and query callback happen here instead.
208    pub fn set_query(
209        &mut self,
210        query: impl Into<SharedString>,
211        window: &mut Window,
212        cx: &mut Context<Self>,
213    ) {
214        let query = query.into();
215        if self.query(cx) == query {
216            return;
217        }
218
219        self.query_input
220            .update(cx, |input, cx| input.set_value(query, window, cx));
221        self.on_query_changed(window, cx);
222    }
223
224    /// The highlighted item's path in the model installed by the latest
225    /// [`crate::command::Command`] render, before local filtering.
226    ///
227    /// Ungrouped items occupy section 0 and use their input position as the
228    /// row. Explicit groups use their group and item positions; when a model
229    /// mixes both forms, the implicit ungrouped section comes first.
230    pub fn selected_index(&self) -> Option<IndexPath> {
231        self.selected_index
232            .and_then(|selected_index| self.matched.get(selected_index))
233            .filter(|matched| !matched.disabled)
234            .map(|matched| matched.index_path)
235    }
236
237    /// Highlight an item by its original, unfiltered model path, or clear the
238    /// highlight with `None`.
239    ///
240    /// A path that is currently filtered out or disabled clears the
241    /// highlight. A visible selection is scrolled into view.
242    pub fn set_selected_index(
243        &mut self,
244        index: Option<IndexPath>,
245        window: &mut Window,
246        cx: &mut Context<Self>,
247    ) {
248        let matched_ix = index.and_then(|index| {
249            self.matched
250                .iter()
251                .position(|matched| matched.index_path == index && !matched.disabled)
252        });
253
254        let preserve_no_selection = matched_ix.is_none();
255        if self.selected_index == matched_ix {
256            self.preserve_no_selection = preserve_no_selection;
257            return;
258        }
259
260        let previous_index = self.selected_index();
261        self.selected_index = matched_ix;
262        self.preserve_no_selection = preserve_no_selection;
263        self.pending_scroll = matched_ix
264            .and_then(|matched_ix| self.matched.get(matched_ix))
265            .map(|matched| matched.row_ix);
266
267        if let Some((on_select, index)) = self.on_select_if_changed(previous_index) {
268            window.defer(cx, move |window, cx| on_select(index, window, cx));
269        }
270
271        cx.notify();
272    }
273
274    /// The number of items matching the current query.
275    pub fn matched_count(&self) -> usize {
276        self.matched.len()
277    }
278
279    /// Move focus to the palette's active control.
280    pub fn focus(&self, window: &mut Window, cx: &mut App) {
281        if self.model.searchable {
282            self.query_input.focus_handle(cx).focus(window, cx);
283        } else {
284            self.focus_handle.focus(window, cx);
285        }
286    }
287
288    /// Show or hide the search field's spinner, and suppress the empty message
289    /// while it spins.
290    ///
291    /// Turn it on while an `on_query` callback is being answered.
292    pub fn set_loading(&mut self, loading: bool, window: &mut Window, cx: &mut Context<Self>) {
293        self.loading = loading;
294        self.query_input
295            .update(cx, |input, cx| input.set_loading(loading, window, cx));
296        cx.notify();
297    }
298
299    /// Whether the search field is showing its spinner.
300    pub fn is_loading(&self) -> bool {
301        self.loading
302    }
303
304    // MARK: Matching
305
306    fn item_matches(&self, item: &CommandItem, query: &str) -> bool {
307        if !self.model.searchable || !self.model.filterable || query.is_empty() {
308            true
309        } else {
310            item.matches(query)
311        }
312    }
313
314    fn item_at(&self, matched_ix: usize) -> Option<&CommandItem> {
315        let matched = self.matched.get(matched_ix)?;
316
317        match self.model.entries.get(matched.entry_ix)? {
318            CommandEntry::Item(item) => Some(item),
319            CommandEntry::Group(group) => group.items.get(matched.item_ix),
320            CommandEntry::Separator => None,
321        }
322    }
323
324    /// Recompute the visible rows and the matching items for the current query.
325    ///
326    fn update_matches(&mut self, cx: &App) {
327        let query = self.query(cx);
328        let query = query.trim();
329
330        let mut rows: Vec<CommandRow> = Vec::new();
331        let mut matched: Vec<MatchedItem> = Vec::new();
332        let has_ungrouped_items = self
333            .model
334            .entries
335            .iter()
336            .any(|entry| matches!(entry, CommandEntry::Item(_)));
337        let mut ungrouped_item_ix = 0;
338        let mut group_ix = 0;
339        // A separator is only drawn once something follows it, which drops the
340        // leading, trailing and doubled separators a filtered list leaves behind.
341        let mut pending_separator = false;
342
343        for (entry_ix, entry) in self.model.entries.iter().enumerate() {
344            match entry {
345                CommandEntry::Separator => pending_separator = !rows.is_empty(),
346                CommandEntry::Item(item) => {
347                    let item_ix = ungrouped_item_ix;
348                    ungrouped_item_ix += 1;
349                    if !self.item_matches(item, query) {
350                        continue;
351                    }
352
353                    if pending_separator {
354                        rows.push(CommandRow::Separator);
355                        pending_separator = false;
356                    }
357
358                    let index_path = IndexPath::new(item_ix).section(0);
359                    matched.push(MatchedItem {
360                        entry_ix,
361                        item_ix: 0,
362                        index_path,
363                        row_ix: rows.len(),
364                        disabled: item.is_disabled(),
365                    });
366                    rows.push(CommandRow::Item(matched.len() - 1));
367                }
368                CommandEntry::Group(group) => {
369                    let section_ix = group_ix + usize::from(has_ungrouped_items);
370                    group_ix += 1;
371                    let visible = group
372                        .items
373                        .iter()
374                        .enumerate()
375                        .filter(|(_, item)| self.item_matches(item, query))
376                        .map(|(item_ix, item)| (item_ix, item.is_disabled()))
377                        .collect::<Vec<_>>();
378
379                    if visible.is_empty() {
380                        continue;
381                    }
382
383                    if pending_separator {
384                        rows.push(CommandRow::Separator);
385                        pending_separator = false;
386                    }
387
388                    if let Some(heading) = group.heading() {
389                        rows.push(CommandRow::Heading(heading.clone()));
390                    }
391
392                    for (item_ix, disabled) in visible {
393                        let index_path = IndexPath::new(item_ix).section(section_ix);
394                        matched.push(MatchedItem {
395                            entry_ix,
396                            item_ix,
397                            index_path,
398                            row_ix: rows.len(),
399                            disabled,
400                        });
401                        rows.push(CommandRow::Item(matched.len() - 1));
402                    }
403                }
404            }
405        }
406
407        self.rows = rows;
408        self.matched = matched;
409        self.needs_measure = true;
410        self.selected_index = self.selected_index.and_then(|selected_index| {
411            (selected_index < self.matched.len()).then_some(selected_index)
412        });
413    }
414
415    /// Move the highlight to the first item that can be confirmed.
416    fn reset_selection(&mut self) {
417        self.selected_index = self.matched.iter().position(|matched| !matched.disabled);
418        self.preserve_no_selection = false;
419        self.pending_scroll = self
420            .selected_index
421            .and_then(|selected_index| self.matched.get(selected_index))
422            .map(|matched| matched.row_ix)
423            .or(Some(0));
424    }
425
426    fn on_query_input_event(
427        &mut self,
428        _: &Entity<InputState>,
429        event: &InputEvent,
430        window: &mut Window,
431        cx: &mut Context<Self>,
432    ) {
433        if !matches!(event, InputEvent::Change) {
434            return;
435        }
436
437        self.on_query_changed(window, cx);
438    }
439
440    /// Re-filter for the query that is now in the field, and report it.
441    fn on_query_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
442        let query = self.query(cx);
443        if query == self.applied_query {
444            return;
445        }
446
447        let previous_selection = self.selected_index();
448        self.applied_query = query.clone();
449        self.update_matches(cx);
450        self.reset_selection();
451        let selection_callback = self.on_select_if_changed(previous_selection);
452        let query_callback = self
453            .model
454            .searchable
455            .then(|| self.model.on_query.clone())
456            .flatten();
457
458        if selection_callback.is_some() || query_callback.is_some() {
459            window.defer(cx, move |window, cx| {
460                if let Some((on_select, index)) = selection_callback {
461                    on_select(index, window, cx);
462                }
463                if let Some(on_query) = query_callback {
464                    on_query(query.as_ref(), window, cx);
465                }
466            });
467        }
468
469        cx.notify();
470    }
471
472    fn set_list_measurement_key(
473        &mut self,
474        measurement_key: ListMeasurementKey,
475        cx: &mut Context<Self>,
476    ) {
477        if self.list_measurement_key.as_ref() == Some(&measurement_key) {
478            return;
479        }
480
481        self.list_measurement_key = Some(measurement_key);
482        self.needs_measure = true;
483        cx.notify();
484    }
485
486    // MARK: Actions
487
488    fn on_select_if_changed(
489        &self,
490        previous_index: Option<IndexPath>,
491    ) -> Option<(Rc<OnIndex>, IndexPath)> {
492        let index = self.selected_index();
493        if index == previous_index {
494            return None;
495        }
496
497        self.model.on_select.clone().zip(index)
498    }
499
500    /// Highlight an item without scrolling it into view. Hover goes through
501    /// here, and revealing a half-clipped edge row would slide the next row
502    /// under the resting cursor, hover-selecting and scrolling in a loop.
503    fn select(&mut self, matched_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
504        if self.selected_index == Some(matched_ix) {
505            return;
506        }
507
508        let previous_index = self.selected_index();
509        self.selected_index = Some(matched_ix);
510        self.preserve_no_selection = false;
511
512        if let Some((on_select, index)) = self.on_select_if_changed(previous_index) {
513            window.defer(cx, move |window, cx| on_select(index, window, cx));
514        }
515
516        cx.notify();
517    }
518
519    /// Move the highlight by `step` items, wrapping around and skipping the
520    /// disabled ones.
521    fn select_by(&mut self, step: isize, window: &mut Window, cx: &mut Context<Self>) {
522        let len = self.matched.len();
523        if len == 0 {
524            return;
525        }
526
527        let mut next = self
528            .selected_index
529            .unwrap_or_else(|| if step >= 0 { len.saturating_sub(1) } else { 0 });
530        let mut enabled = None;
531        for _ in 0..len {
532            next = (next as isize + step).rem_euclid(len as isize) as usize;
533            if !self.matched[next].disabled {
534                enabled = Some(next);
535                break;
536            }
537        }
538
539        if let Some(next) = enabled
540            && self.selected_index != Some(next)
541        {
542            self.pending_scroll = self.matched.get(next).map(|matched| matched.row_ix);
543            self.select(next, window, cx);
544        }
545    }
546
547    fn on_action_select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
548        self.select_by(-1, window, cx);
549    }
550
551    fn on_action_select_down(
552        &mut self,
553        _: &SelectDown,
554        window: &mut Window,
555        cx: &mut Context<Self>,
556    ) {
557        self.select_by(1, window, cx);
558    }
559
560    fn on_action_confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
561        if let Some(selected_index) = self.selected_index {
562            self.confirm(selected_index, window, cx);
563        }
564    }
565
566    /// Escape clears a non-empty query first, and only then leaves the palette
567    /// — the dialog that hosts it closes on the second press.
568    fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
569        if self.model.searchable && !self.query(cx).is_empty() {
570            self.set_query("", window, cx);
571            return;
572        }
573
574        // Cancel is the one synchronous callback: propagation must continue in
575        // this dispatch so a hosting Dialog observes it once and owns the pop.
576        if let Some(on_cancel) = self.model.on_cancel.clone() {
577            on_cancel(window, cx);
578        }
579
580        cx.propagate();
581    }
582
583    fn confirm(&mut self, matched_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
584        let Some(item) = self.item_at(matched_ix) else {
585            return;
586        };
587        if item.is_disabled() {
588            return;
589        }
590
591        let index_path = self.matched[matched_ix].index_path;
592        let action = item.action.as_ref().map(|action| action.boxed_clone());
593        let on_confirm = self.model.on_confirm.clone();
594
595        if let Some(action) = action {
596            window.dispatch_action(action, cx);
597        }
598        if let Some(on_confirm) = on_confirm {
599            window.defer(cx, move |window, cx| {
600                on_confirm(index_path, window, cx);
601            });
602        }
603    }
604
605    // MARK: Row sizing
606
607    /// Measure each row before passing the sizes to the virtual list. Custom
608    /// item elements can have independent intrinsic heights.
609    fn measure_row_sizes(&self, window: &mut Window, cx: &mut Context<Self>) -> Vec<Size<Pixels>> {
610        let available = size(
611            self.list_measurement_key
612                .as_ref()
613                .map_or(AvailableSpace::MinContent, |key| {
614                    AvailableSpace::Definite(key.content_width)
615                }),
616            AvailableSpace::MinContent,
617        );
618        let mut text_style = StyleRefinement::default();
619        text_style.text = self.options.style.text.clone();
620
621        self.rows
622            .iter()
623            .enumerate()
624            .map(|(row_ix, row)| match row {
625                CommandRow::Separator => size(px(0.), px(SEPARATOR_ROW_HEIGHT)),
626                CommandRow::Heading(_) | CommandRow::Item(_) => {
627                    let row_size = div()
628                        .refine_style(&text_style)
629                        .child(self.render_row(row_ix, window, cx))
630                        .into_any_element()
631                        .layout_as_root(available, window, cx);
632                    size(px(0.), row_size.height)
633                }
634            })
635            .collect()
636    }
637
638    // MARK: Rendering
639
640    fn sync_placeholder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
641        let placeholder = self
642            .options
643            .placeholder
644            .as_ref()
645            .cloned()
646            .unwrap_or_else(|| t!("Command.placeholder").to_string().into());
647
648        if self.applied_placeholder == placeholder {
649            return;
650        }
651
652        self.applied_placeholder = placeholder.clone();
653        self.query_input.update(cx, |input, cx| {
654            input.set_placeholder(placeholder, window, cx)
655        });
656    }
657
658    /// The frame every item row shares, so that the measured height matches the
659    /// rendered one.
660    fn item_row(&self, selected: bool, cx: &App) -> gpui::Div {
661        div()
662            .flex()
663            .flex_row()
664            .items_center()
665            .w_full()
666            .gap_2()
667            .px_2()
668            .py_1p5()
669            .text_sm()
670            .rounded(cx.theme().radius)
671            .when(selected, |this| {
672                this.bg(cx.theme().accent)
673                    .text_color(cx.theme().accent_foreground)
674            })
675    }
676
677    fn heading_row(&self, heading: SharedString, cx: &App) -> gpui::Div {
678        div()
679            .w_full()
680            .px_2()
681            .py_1p5()
682            .text_xs()
683            .font_medium()
684            .text_color(cx.theme().muted_foreground)
685            .child(heading)
686    }
687
688    fn render_row(&self, row_ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
689        match self.rows.get(row_ix) {
690            None => div().into_any_element(),
691            Some(CommandRow::Separator) => div()
692                .w_full()
693                .py(px(4.))
694                .child(div().h(px(1.)).w_full().bg(cx.theme().border))
695                .into_any_element(),
696            Some(CommandRow::Heading(heading)) => {
697                self.heading_row(heading.clone(), cx).into_any_element()
698            }
699            Some(CommandRow::Item(matched_ix)) => self.render_item(*matched_ix, window, cx),
700        }
701    }
702
703    fn render_item(
704        &self,
705        matched_ix: usize,
706        window: &mut Window,
707        cx: &mut Context<Self>,
708    ) -> AnyElement {
709        let Some(item) = self.item_at(matched_ix) else {
710            return div().into_any_element();
711        };
712
713        let disabled = item.is_disabled();
714        let selected = self.selected_index == Some(matched_ix) && !disabled;
715        let muted_foreground = cx.theme().muted_foreground;
716        let icon_color = if selected {
717            cx.theme().accent_foreground
718        } else {
719            muted_foreground
720        };
721        let binding = if item.content.is_none() {
722            item.action.as_ref().and_then(|action| {
723                Kbd::binding_for_action_in(action.as_ref(), &self.focus_handle(cx), window)
724                    .or_else(|| Kbd::binding_for_action(action.as_ref(), None, window))
725            })
726        } else {
727            None
728        };
729
730        let content = match &item.content {
731            Some(render) => render(window, cx),
732            None => h_flex()
733                .flex_1()
734                .gap_2()
735                .items_center()
736                .when_some(item.icon.clone(), |this, icon| {
737                    this.child(icon.size_4().text_color(icon_color))
738                })
739                .when_some(item.label_text().cloned(), |this, label| this.child(label))
740                .into_any_element(),
741        };
742
743        self.item_row(selected, cx)
744            .id(self.matched[matched_ix].index_path)
745            .role(Role::ListBoxOption)
746            .aria_selected(selected)
747            .when(disabled, |this| this.text_color(muted_foreground))
748            .when(!disabled, |this| {
749                this.cursor_default()
750                    .on_hover(cx.listener(move |this, hovered: &bool, window, cx| {
751                        if *hovered {
752                            this.select(matched_ix, window, cx);
753                        }
754                    }))
755                    .on_click(cx.listener(move |this, _, window, cx| {
756                        this.confirm(matched_ix, window, cx);
757                    }))
758            })
759            .child(content)
760            .map(|this| match binding {
761                Some(binding) => this.child(binding.ml_auto()),
762                // The binding owns the trailing slot, so only an item without
763                // one can show its check there.
764                None => this.when(item.checked, |this| {
765                    this.child(crate::Sizable::xsmall(Icon::new(IconName::Check).ml_auto()))
766                }),
767            })
768            .into_any_element()
769    }
770
771    fn render_empty(&self, window: &mut Window, cx: &mut App) -> AnyElement {
772        if let Some(empty) = self.options.empty.as_ref() {
773            return empty(self, window, cx);
774        }
775
776        let message: SharedString = t!("Command.empty").to_string().into();
777
778        div()
779            .py_6()
780            .w_full()
781            .text_center()
782            .text_sm()
783            .text_color(cx.theme().muted_foreground)
784            .child(message)
785            .into_any_element()
786    }
787}
788
789impl Focusable for CommandState {
790    fn focus_handle(&self, cx: &App) -> FocusHandle {
791        if self.model.searchable {
792            self.query_input.focus_handle(cx)
793        } else {
794            self.focus_handle.clone()
795        }
796    }
797}
798
799impl Render for CommandState {
800    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
801        self.sync_placeholder(window, cx);
802
803        if self.needs_measure {
804            self.needs_measure = false;
805            self.row_sizes = Rc::new(self.measure_row_sizes(window, cx));
806        }
807
808        if let Some(row_ix) = self.pending_scroll.take() {
809            self.scroll_handle
810                .scroll_to_item(row_ix, ScrollStrategy::Nearest);
811        }
812
813        let rows_count = self.rows.len();
814        let row_sizes = self.row_sizes.clone();
815        let command_state = cx.entity();
816
817        v_flex()
818            .id("command")
819            .key_context(CONTEXT)
820            .track_focus(&self.focus_handle)
821            .on_action(cx.listener(Self::on_action_select_up))
822            .on_action(cx.listener(Self::on_action_select_down))
823            .on_action(cx.listener(Self::on_action_confirm))
824            .on_action(cx.listener(Self::on_action_cancel))
825            .w_full()
826            .overflow_hidden()
827            .bg(cx.theme().popover)
828            .text_color(cx.theme().popover_foreground)
829            .when(self.options.bordered, |this| {
830                this.rounded(cx.theme().radius_lg)
831                    .border_1()
832                    .border_color(cx.theme().border)
833            })
834            .refine_style(&self.options.style)
835            .when_some(self.options.header.as_ref(), |this, header| {
836                this.child(header(self, window, cx))
837            })
838            .when(self.model.searchable, |this| {
839                this.child(
840                    div()
841                        .flex_none()
842                        .px_3()
843                        .border_b_1()
844                        .border_color(cx.theme().border)
845                        .child(
846                            Input::new(&self.query_input)
847                                .prefix(
848                                    Icon::new(IconName::Search)
849                                        .text_color(cx.theme().muted_foreground),
850                                )
851                                .appearance(false)
852                                .p_0(),
853                        ),
854                )
855            })
856            .child(
857                v_flex()
858                    .id("command-list-container")
859                    .role(Role::ListBox)
860                    .relative()
861                    .flex_1()
862                    // The rows carry their inset on the virtual list itself so
863                    // that a mid-scroll clip edge sits flush against the
864                    // surrounding dividers; only the empty slot needs the
865                    // container padding.
866                    .when(rows_count == 0, |this| this.p_1())
867                    .on_prepaint({
868                        let measure_state = command_state.clone();
869                        move |bounds, window, cx| {
870                            measure_state.update(cx, |state, cx| {
871                                // The list's `p_1` is one quarter rem on each
872                                // side. Its rem-dependent padding and inherited
873                                // layout-relevant text style participate in
874                                // the row-size cache key.
875                                let text_style = window.text_style();
876                                state.set_list_measurement_key(
877                                    ListMeasurementKey {
878                                        content_width: (bounds.size.width
879                                            - window.rem_size() * 0.5)
880                                            .max(px(0.)),
881                                        rem_size: window.rem_size(),
882                                        line_height: window.line_height(),
883                                        text_shape: TextShapeKey {
884                                            font_family: text_style.font_family,
885                                            font_features: text_style.font_features,
886                                            font_fallbacks: text_style.font_fallbacks,
887                                            font_size: text_style.font_size,
888                                            font_weight: text_style.font_weight,
889                                            font_style: text_style.font_style,
890                                            white_space: text_style.white_space,
891                                            text_overflow: text_style.text_overflow,
892                                            line_clamp: text_style.line_clamp,
893                                        },
894                                    },
895                                    cx,
896                                )
897                            })
898                        }
899                    })
900                    .max_h(self.options.max_h)
901                    .overflow_hidden()
902                    // While a search is in flight the list is empty because the
903                    // answer has not arrived, which is not the same as no match.
904                    .when(rows_count == 0 && !self.loading, |this| {
905                        this.child(self.render_empty(window, cx))
906                    })
907                    .when(rows_count > 0, |this| {
908                        this.child(
909                            v_virtual_list(
910                                command_state.clone(),
911                                "command-list",
912                                row_sizes,
913                                move |this, visible_range, window, cx| {
914                                    visible_range
915                                        .map(|row_ix| this.render_row(row_ix, window, cx))
916                                        .collect::<Vec<_>>()
917                                },
918                            )
919                            // Padding on the virtual list acts like CSS
920                            // scroll-padding: the scroll ends keep their inset
921                            // while scrolled-under rows paint and clip at the
922                            // list edge.
923                            .p_1()
924                            .with_sizing_behavior(ListSizingBehavior::Infer)
925                            .track_scroll(&self.scroll_handle),
926                        )
927                        .child(Scrollbar::vertical(&self.scroll_handle))
928                    }),
929            )
930            .when_some(self.options.footer.as_ref(), |this, footer| {
931                this.child(footer(self, window, cx))
932            })
933    }
934}
935
936// MARK: Tests
937
938#[cfg(test)]
939mod tests {
940    use std::{
941        cell::{Cell, RefCell},
942        rc::Rc,
943    };
944
945    use gpui::{
946        AppContext as _, AvailableSpace, Entity, InteractiveElement as _, IntoElement, KeyBinding,
947        Modifiers, ParentElement as _, Pixels, Render, Styled as _, TestAppContext, Window,
948        actions, div, point, prelude::FluentBuilder as _, px,
949    };
950
951    use super::{CONTEXT, CommandModel, CommandRow, CommandState, SEPARATOR_ROW_HEIGHT};
952    use crate::{
953        Disableable as _, IndexPath,
954        actions::{Cancel, Confirm, SelectDown},
955        command::{Command, CommandEntry, CommandGroup, CommandItem},
956    };
957
958    actions!(
959        command_test,
960        [GlobalTestItem, OpenTestItem, RemovePaletteTestItem]
961    );
962
963    struct CommandActionsHarness {
964        state: Entity<CommandState>,
965        events: Rc<RefCell<Vec<String>>>,
966    }
967
968    impl Render for CommandActionsHarness {
969        fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
970            let action_events = self.events.clone();
971            let propagated_cancel_events = self.events.clone();
972            let query_events = self.events.clone();
973            let select_events = self.events.clone();
974            let confirm_events = self.events.clone();
975            let cancel_events = self.events.clone();
976
977            div()
978                .size_full()
979                .on_action(move |_: &OpenTestItem, _, _| {
980                    action_events.borrow_mut().push("action".into());
981                })
982                .on_action(move |_: &Cancel, _, _| {
983                    propagated_cancel_events
984                        .borrow_mut()
985                        .push("propagated_cancel".into());
986                })
987                .child(
988                    Command::new(&self.state)
989                        .item(
990                            CommandItem::new()
991                                .label("Item")
992                                .keywords(["needle"])
993                                .action(Box::new(OpenTestItem)),
994                        )
995                        .item(CommandItem::new().label("Item"))
996                        .item(
997                            CommandItem::new()
998                                .label("Item")
999                                .action(Box::new(GlobalTestItem)),
1000                        )
1001                        .on_query(move |query, _, _| {
1002                            query_events.borrow_mut().push(format!("query:{query}"));
1003                        })
1004                        .on_select(move |index, _, _| {
1005                            select_events
1006                                .borrow_mut()
1007                                .push(format!("select:{}:{}", index.section, index.row));
1008                        })
1009                        .on_confirm(move |index, _, _| {
1010                            confirm_events
1011                                .borrow_mut()
1012                                .push(format!("confirm:{}:{}", index.section, index.row));
1013                        })
1014                        .on_cancel(move |_, _| {
1015                            cancel_events.borrow_mut().push("cancel".into());
1016                        }),
1017                )
1018        }
1019    }
1020
1021    struct ReentrantCallbackHarness {
1022        state: Entity<CommandState>,
1023        events: Vec<String>,
1024    }
1025
1026    impl Render for ReentrantCallbackHarness {
1027        fn render(&mut self, _: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
1028            let select_owner = cx.weak_entity();
1029            let query_owner = cx.weak_entity();
1030            let confirm_owner = cx.weak_entity();
1031
1032            Command::new(&self.state)
1033                .item(CommandItem::new().label("alpha"))
1034                .item(CommandItem::new().label("beta"))
1035                .on_select(move |index, _, cx| {
1036                    _ = select_owner.update(cx, |harness, cx| {
1037                        assert_eq!(harness.state.read(cx).selected_index(), Some(index));
1038                        harness
1039                            .events
1040                            .push(format!("select:{}:{}", index.section, index.row));
1041                    });
1042                })
1043                .on_query(move |query, _, cx| {
1044                    _ = query_owner.update(cx, |harness, cx| {
1045                        assert_eq!(harness.state.read(cx).query(cx).as_ref(), query);
1046                        harness.events.push(format!("query:{query}"));
1047                    });
1048                })
1049                .on_confirm(move |index, _, cx| {
1050                    _ = confirm_owner.update(cx, |harness, cx| {
1051                        assert_eq!(harness.state.read(cx).selected_index(), Some(index));
1052                        harness
1053                            .events
1054                            .push(format!("confirm:{}:{}", index.section, index.row));
1055                    });
1056                })
1057        }
1058    }
1059
1060    #[gpui::test]
1061    fn query_and_selection_callbacks_run_after_the_state_lease_in_defined_order(
1062        cx: &mut TestAppContext,
1063    ) {
1064        cx.update(crate::init);
1065        let (harness, cx) = cx.add_window_view(|window, cx| ReentrantCallbackHarness {
1066            state: cx.new(|cx| CommandState::new(window, cx)),
1067            events: Vec::new(),
1068        });
1069        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1070
1071        cx.run_until_parked();
1072        cx.update(|window, cx| {
1073            _ = window.draw(cx);
1074            state.update(cx, |state, cx| {
1075                state.selected_index = Some(1);
1076                state.set_query("alpha", window, cx);
1077            });
1078        });
1079
1080        assert_eq!(
1081            harness.read_with(cx, |harness, _| harness.events.clone()),
1082            ["select:0:0", "query:alpha"]
1083        );
1084    }
1085
1086    #[gpui::test]
1087    fn actionless_confirm_callback_runs_after_the_state_lease(cx: &mut TestAppContext) {
1088        cx.update(crate::init);
1089        let (harness, cx) = cx.add_window_view(|window, cx| ReentrantCallbackHarness {
1090            state: cx.new(|cx| CommandState::new(window, cx)),
1091            events: Vec::new(),
1092        });
1093        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1094
1095        cx.run_until_parked();
1096        cx.update(|window, cx| {
1097            _ = window.draw(cx);
1098            state.update(cx, |state, cx| state.confirm(0, window, cx));
1099        });
1100
1101        assert_eq!(
1102            harness.read_with(cx, |harness, _| harness.events.clone()),
1103            ["confirm:0:0"]
1104        );
1105    }
1106
1107    struct CommandItemWidthHarness {
1108        state: Entity<CommandState>,
1109        matched_ix: usize,
1110        width: Rc<Cell<Option<Pixels>>>,
1111    }
1112
1113    impl Render for CommandItemWidthHarness {
1114        fn render(
1115            &mut self,
1116            window: &mut Window,
1117            cx: &mut gpui::Context<Self>,
1118        ) -> impl IntoElement {
1119            let width = self.width.clone();
1120            let item = self.state.update(cx, |state, cx| {
1121                state.render_item(self.matched_ix, window, cx)
1122            });
1123
1124            div()
1125                .on_children_prepainted(move |bounds, _, _| width.set(Some(bounds[0].size.width)))
1126                .child(item)
1127        }
1128    }
1129
1130    #[gpui::test]
1131    fn action_that_removes_command_state_still_confirms_after_dispatch(cx: &mut TestAppContext) {
1132        cx.update(crate::init);
1133        let events = Rc::new(RefCell::new(Vec::new()));
1134        let state_owner: Rc<RefCell<Option<Entity<CommandState>>>> = Rc::new(RefCell::new(None));
1135        let action_events = events.clone();
1136        let action_state_owner = state_owner.clone();
1137        cx.update(|cx| {
1138            cx.on_action(move |_: &RemovePaletteTestItem, _| {
1139                action_events.borrow_mut().push("action".into());
1140                action_state_owner.borrow_mut().take();
1141            });
1142        });
1143        let cx = cx.add_empty_window();
1144        cx.update(|window, cx| {
1145            let confirm_events = events.clone();
1146            let state = cx.new(|cx| {
1147                let mut state = CommandState::new(window, cx);
1148                state.install_model(
1149                    CommandModel {
1150                        entries: vec![CommandEntry::Item(
1151                            CommandItem::new()
1152                                .label("removed")
1153                                .action(Box::new(RemovePaletteTestItem)),
1154                        )],
1155                        searchable: false,
1156                        on_confirm: Some(Rc::new(move |index, _, _| {
1157                            confirm_events
1158                                .borrow_mut()
1159                                .push(format!("confirm:{}:{}", index.section, index.row));
1160                        })),
1161                        ..CommandModel::default()
1162                    },
1163                    cx,
1164                );
1165                state
1166            });
1167            *state_owner.borrow_mut() = Some(state.clone());
1168            state.update(cx, |state, cx| state.confirm(0, window, cx));
1169        });
1170        cx.run_until_parked();
1171
1172        assert!(state_owner.borrow().is_none());
1173        assert_eq!(events.borrow().as_slice(), ["action", "confirm:0:0"]);
1174    }
1175
1176    #[gpui::test]
1177    fn command_actions_and_callbacks_follow_defined_order(cx: &mut TestAppContext) {
1178        cx.update(|cx| {
1179            crate::init(cx);
1180            cx.bind_keys([
1181                KeyBinding::new("ctrl-o", OpenTestItem, Some(CONTEXT)),
1182                KeyBinding::new("ctrl-g", GlobalTestItem, None),
1183            ]);
1184        });
1185        let events = Rc::new(RefCell::new(Vec::new()));
1186        let (harness, cx) = cx.add_window_view(|window, cx| CommandActionsHarness {
1187            state: cx.new(|cx| CommandState::new(window, cx)),
1188            events: events.clone(),
1189        });
1190        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1191
1192        cx.run_until_parked();
1193        cx.update(|window, cx| {
1194            _ = window.draw(cx);
1195            state.update(cx, |state, cx| state.focus(window, cx));
1196            _ = window.draw(cx);
1197        });
1198
1199        let action_width = Rc::new(Cell::new(None));
1200        let plain_width = Rc::new(Cell::new(None));
1201        let global_width = Rc::new(Cell::new(None));
1202        let (action_probe, plain_probe, global_probe) = cx.update(|_, cx| {
1203            (
1204                cx.new(|_| CommandItemWidthHarness {
1205                    state: state.clone(),
1206                    matched_ix: 0,
1207                    width: action_width.clone(),
1208                }),
1209                cx.new(|_| CommandItemWidthHarness {
1210                    state: state.clone(),
1211                    matched_ix: 1,
1212                    width: plain_width.clone(),
1213                }),
1214                cx.new(|_| CommandItemWidthHarness {
1215                    state: state.clone(),
1216                    matched_ix: 2,
1217                    width: global_width.clone(),
1218                }),
1219            )
1220        });
1221        cx.draw(
1222            point(px(0.), px(0.)),
1223            AvailableSpace::min_size(),
1224            move |_, _| action_probe.into_any_element(),
1225        );
1226        cx.draw(
1227            point(px(0.), px(0.)),
1228            AvailableSpace::min_size(),
1229            move |_, _| plain_probe.into_any_element(),
1230        );
1231        cx.draw(
1232            point(px(0.), px(0.)),
1233            AvailableSpace::min_size(),
1234            move |_, _| global_probe.into_any_element(),
1235        );
1236        let action_width = action_width.get().unwrap();
1237        let plain_width = plain_width.get().unwrap();
1238        let global_width = global_width.get().unwrap();
1239        assert!(
1240            action_width > plain_width,
1241            "the scoped Action binding should add a visible Kbd ({action_width:?} vs {plain_width:?})",
1242        );
1243        assert!(
1244            global_width > plain_width,
1245            "the app-level fallback binding should add a visible Kbd ({global_width:?} vs {plain_width:?})",
1246        );
1247
1248        cx.update(|window, cx| {
1249            state.update(cx, |state, cx| {
1250                state.set_query("needle", window, cx);
1251                state.set_query("needle", window, cx);
1252                state.set_query("", window, cx);
1253            });
1254            window.dispatch_action(Box::new(SelectDown), cx);
1255            window.dispatch_action(Box::new(crate::actions::SelectUp), cx);
1256            window.dispatch_action(Box::new(Confirm { secondary: false }), cx);
1257        });
1258        cx.run_until_parked();
1259
1260        assert_eq!(
1261            events.borrow().as_slice(),
1262            [
1263                "query:needle",
1264                "query:",
1265                "select:0:1",
1266                "select:0:0",
1267                "action",
1268                "confirm:0:0",
1269            ]
1270        );
1271
1272        cx.simulate_click(point(px(20.), px(52.)), Modifiers::default());
1273        cx.run_until_parked();
1274        cx.update(|window, cx| window.dispatch_action(Box::new(Cancel), cx));
1275        cx.run_until_parked();
1276
1277        assert_eq!(
1278            events.borrow().as_slice(),
1279            [
1280                "query:needle",
1281                "query:",
1282                "select:0:1",
1283                "select:0:0",
1284                "action",
1285                "confirm:0:0",
1286                "action",
1287                "confirm:0:0",
1288                "cancel",
1289                "propagated_cancel",
1290            ]
1291        );
1292    }
1293
1294    struct CommandOwnedEntriesHarness {
1295        state: Entity<CommandState>,
1296    }
1297
1298    impl Render for CommandOwnedEntriesHarness {
1299        fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
1300            Command::new(&self.state)
1301                .searchable(false)
1302                .item(CommandItem::new().label("alpha"))
1303                .group(
1304                    CommandGroup::new()
1305                        .label("Settings")
1306                        .item(CommandItem::new().label("beta")),
1307                )
1308                .separator()
1309                .item(
1310                    CommandItem::new()
1311                        .label("custom")
1312                        .child(|_, _| div().h(px(72.)).child("Custom")),
1313                )
1314        }
1315    }
1316
1317    #[gpui::test]
1318    fn command_owns_entries_and_lazy_item_content(cx: &mut TestAppContext) {
1319        cx.update(crate::init);
1320        let (harness, cx) = cx.add_window_view(|window, cx| CommandOwnedEntriesHarness {
1321            state: cx.new(|cx| CommandState::new(window, cx)),
1322        });
1323
1324        cx.run_until_parked();
1325        cx.update(|window, cx| _ = window.draw(cx));
1326
1327        let (labels, rows, row_sizes) = cx.update(|_, cx| {
1328            let state = harness.read(cx).state.read(cx);
1329            (
1330                (0..state.matched_count())
1331                    .map(|matched_ix| {
1332                        state
1333                            .item_at(matched_ix)
1334                            .unwrap()
1335                            .label_text()
1336                            .unwrap()
1337                            .clone()
1338                    })
1339                    .collect::<Vec<_>>(),
1340                state.rows.clone(),
1341                state.row_sizes.clone(),
1342            )
1343        });
1344
1345        assert_eq!(labels, ["alpha", "beta", "custom"]);
1346        assert!(matches!(
1347            rows.as_slice(),
1348            [
1349                CommandRow::Item(_),
1350                CommandRow::Heading(heading),
1351                CommandRow::Item(_),
1352                CommandRow::Separator,
1353                CommandRow::Item(_),
1354            ] if heading == "Settings"
1355        ));
1356        assert_eq!(row_sizes[4].height, px(84.));
1357    }
1358
1359    fn command_with_entries(
1360        state: &Entity<CommandState>,
1361        entries: impl IntoIterator<Item = CommandEntry>,
1362    ) -> Command {
1363        entries
1364            .into_iter()
1365            .fold(Command::new(state), |command, entry| match entry {
1366                CommandEntry::Item(item) => command.item(item),
1367                CommandEntry::Group(group) => command.group(group),
1368                CommandEntry::Separator => command.separator(),
1369            })
1370    }
1371
1372    fn command_state(
1373        window: &mut Window,
1374        cx: &mut gpui::Context<CommandState>,
1375        entries: impl IntoIterator<Item = CommandEntry>,
1376    ) -> CommandState {
1377        let mut state = CommandState::new(window, cx);
1378        state.install_model(
1379            CommandModel {
1380                entries: entries.into_iter().collect(),
1381                ..CommandModel::default()
1382            },
1383            cx,
1384        );
1385        state
1386    }
1387
1388    fn command_state_with_options(
1389        window: &mut Window,
1390        cx: &mut gpui::Context<CommandState>,
1391        entries: impl IntoIterator<Item = CommandEntry>,
1392        searchable: bool,
1393    ) -> CommandState {
1394        let mut state = CommandState::new(window, cx);
1395        state.install_model(
1396            CommandModel {
1397                entries: entries.into_iter().collect(),
1398                searchable,
1399                ..CommandModel::default()
1400            },
1401            cx,
1402        );
1403        state
1404    }
1405
1406    fn suggestion_entries() -> Vec<CommandEntry> {
1407        vec![
1408            CommandGroup::new()
1409                .label("Suggestions")
1410                .item(CommandItem::new().label("Calendar"))
1411                .item(CommandItem::new().label("Search Emoji"))
1412                .item(CommandItem::new().label("Calculator").disabled(true))
1413                .into(),
1414            CommandEntry::Separator,
1415            CommandGroup::new()
1416                .label("Settings")
1417                .item(CommandItem::new().label("Profile"))
1418                .item(CommandItem::new().label("Billing"))
1419                .into(),
1420        ]
1421    }
1422
1423    #[gpui::test]
1424    fn query_hides_the_groups_that_have_no_match(cx: &mut TestAppContext) {
1425        cx.update(crate::init);
1426        let cx = cx.add_empty_window();
1427
1428        cx.update(|window, cx| {
1429            let state = cx.new(|cx| command_state(window, cx, suggestion_entries()));
1430
1431            state.update(cx, |state, cx| {
1432                state.update_matches(cx);
1433                assert_eq!(state.matched_count(), 5);
1434                assert_eq!(
1435                    state
1436                        .rows
1437                        .iter()
1438                        .filter(|row| matches!(row, CommandRow::Heading(_)))
1439                        .count(),
1440                    2,
1441                );
1442                assert_eq!(
1443                    state
1444                        .rows
1445                        .iter()
1446                        .filter(|row| matches!(row, CommandRow::Separator))
1447                        .count(),
1448                    1,
1449                );
1450
1451                // "Bil" only matches an item of the second group, so the first
1452                // group's heading and the separator between them both go.
1453                state.set_query("Bil", window, cx);
1454                state.update_matches(cx);
1455
1456                assert_eq!(state.matched_count(), 1);
1457                assert_eq!(state.selected_index(), Some(IndexPath::new(1).section(1)));
1458                assert_eq!(
1459                    state
1460                        .rows
1461                        .iter()
1462                        .filter(|row| matches!(row, CommandRow::Separator))
1463                        .count(),
1464                    0,
1465                );
1466                assert!(matches!(state.rows.first(), Some(CommandRow::Heading(_))));
1467            });
1468        });
1469    }
1470
1471    #[gpui::test]
1472    fn a_query_that_matches_nothing_leaves_no_rows(cx: &mut TestAppContext) {
1473        cx.update(crate::init);
1474        let cx = cx.add_empty_window();
1475
1476        cx.update(|window, cx| {
1477            let state = cx.new(|cx| command_state(window, cx, suggestion_entries()));
1478
1479            state.update(cx, |state, cx| {
1480                state.set_query("zzz", window, cx);
1481                state.update_matches(cx);
1482
1483                assert_eq!(state.matched_count(), 0);
1484                assert!(state.rows.is_empty());
1485                assert_eq!(state.selected_index(), None);
1486            });
1487        });
1488    }
1489
1490    #[gpui::test]
1491    fn filterable_off_keeps_every_item_and_resets_the_highlight(cx: &mut TestAppContext) {
1492        cx.update(crate::init);
1493        let cx = cx.add_empty_window();
1494
1495        cx.update(|window, cx| {
1496            let state = cx.new(|cx| {
1497                let mut state = CommandState::new(window, cx);
1498                state.install_model(
1499                    CommandModel {
1500                        entries: suggestion_entries(),
1501                        filterable: false,
1502                        ..CommandModel::default()
1503                    },
1504                    cx,
1505                );
1506                state
1507            });
1508
1509            state.update(cx, |state, cx| {
1510                state.set_selected_index(Some(IndexPath::new(1).section(1)), window, cx);
1511
1512                // "Bil" would locally match only "Billing"; an unfiltered
1513                // palette keeps every row and hands the highlight back to the
1514                // first item instead of the textual match.
1515                state.set_query("Bil", window, cx);
1516
1517                assert_eq!(state.matched_count(), 5);
1518                assert_eq!(state.selected_index(), Some(IndexPath::new(0).section(0)));
1519            });
1520        });
1521    }
1522
1523    #[gpui::test]
1524    fn keywords_match_when_the_label_does_not(cx: &mut TestAppContext) {
1525        cx.update(crate::init);
1526        let cx = cx.add_empty_window();
1527
1528        cx.update(|window, cx| {
1529            let state = cx.new(|cx| {
1530                command_state(
1531                    window,
1532                    cx,
1533                    [CommandEntry::Item(
1534                        CommandItem::new().label("Profile").keywords(["account"]),
1535                    )],
1536                )
1537            });
1538
1539            state.update(cx, |state, cx| {
1540                state.set_query("account", window, cx);
1541                state.update_matches(cx);
1542
1543                assert_eq!(state.matched_count(), 1);
1544            });
1545        });
1546    }
1547
1548    #[gpui::test]
1549    fn non_searchable_command_keeps_every_item(cx: &mut TestAppContext) {
1550        cx.update(crate::init);
1551        let cx = cx.add_empty_window();
1552        cx.update(|window, cx| {
1553            let state = cx.new(|cx| {
1554                command_state_with_options(
1555                    window,
1556                    cx,
1557                    [
1558                        CommandEntry::Item(CommandItem::new().label("alpha")),
1559                        CommandEntry::Item(CommandItem::new().label("beta")),
1560                    ],
1561                    false,
1562                )
1563            });
1564            state.update(cx, |state, cx| {
1565                state.set_query("missing", window, cx);
1566                assert_eq!(state.matched_count(), 2);
1567            });
1568        });
1569    }
1570
1571    #[gpui::test]
1572    fn non_searchable_command_uses_frame_focus(cx: &mut TestAppContext) {
1573        cx.update(crate::init);
1574        let confirmed = Rc::new(RefCell::new(None));
1575        let confirmed_for_render = confirmed.clone();
1576        let (harness, cx) = cx.add_window_view(move |window, cx| Harness {
1577            state: cx.new(|cx| CommandState::new(window, cx)),
1578            command: Rc::new(move |state| {
1579                let confirmed = confirmed_for_render.clone();
1580                Command::new(state)
1581                    .searchable(false)
1582                    .item(CommandItem::new().label("alpha"))
1583                    .item(CommandItem::new().label("beta"))
1584                    .on_confirm(move |index_path, _, _| {
1585                        *confirmed.borrow_mut() = Some(index_path);
1586                    })
1587            }),
1588        });
1589        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1590
1591        cx.run_until_parked();
1592        cx.update(|window, cx| _ = window.draw(cx));
1593        cx.update(|window, cx| {
1594            state.update(cx, |state, cx| state.focus(window, cx));
1595            assert!(state.read(cx).focus_handle.is_focused(window));
1596            window.dispatch_action(Box::new(SelectDown), cx);
1597            window.dispatch_action(Box::new(Confirm { secondary: false }), cx);
1598        });
1599
1600        assert_eq!(*confirmed.borrow(), Some(IndexPath::new(1).section(0)));
1601    }
1602
1603    #[gpui::test]
1604    fn filtered_ungrouped_item_keeps_its_input_row(cx: &mut TestAppContext) {
1605        cx.update(crate::init);
1606        let confirmed = Rc::new(RefCell::new(None));
1607        let confirmed_for_render = confirmed.clone();
1608        let (harness, cx) = cx.add_window_view(move |window, cx| Harness {
1609            state: cx.new(|cx| CommandState::new(window, cx)),
1610            command: Rc::new(move |state| {
1611                let confirmed = confirmed_for_render.clone();
1612                Command::new(state)
1613                    .items([
1614                        CommandItem::new().label("alpha"),
1615                        CommandItem::new().label("beta"),
1616                        CommandItem::new().label("gamma"),
1617                    ])
1618                    .on_confirm(move |index_path, _, _| {
1619                        *confirmed.borrow_mut() = Some(index_path);
1620                    })
1621            }),
1622        });
1623        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1624
1625        cx.run_until_parked();
1626        cx.update(|window, cx| {
1627            state.update(cx, |state, cx| {
1628                state.set_query("gamma", window, cx);
1629                state.focus(window, cx);
1630            });
1631            window.dispatch_action(Box::new(Confirm { secondary: false }), cx);
1632        });
1633
1634        assert_eq!(*confirmed.borrow(), Some(IndexPath::new(2).section(0)));
1635    }
1636
1637    #[gpui::test]
1638    fn initially_rendered_disabled_first_item_selects_and_confirms_the_first_enabled_item(
1639        cx: &mut TestAppContext,
1640    ) {
1641        cx.update(crate::init);
1642        let confirmed = Rc::new(RefCell::new(None));
1643        let confirmed_for_render = confirmed.clone();
1644        let (harness, cx) = cx.add_window_view(move |window, cx| Harness {
1645            state: cx.new(|cx| CommandState::new(window, cx)),
1646            command: Rc::new(move |state| {
1647                let confirmed = confirmed_for_render.clone();
1648                Command::new(state)
1649                    .item(CommandItem::new().label("disabled").disabled(true))
1650                    .item(CommandItem::new().label("enabled"))
1651                    .on_confirm(move |index_path, _, _| {
1652                        *confirmed.borrow_mut() = Some(index_path);
1653                    })
1654            }),
1655        });
1656        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1657
1658        cx.run_until_parked();
1659        cx.update(|window, cx| _ = window.draw(cx));
1660        cx.update(|window, cx| {
1661            state.update(cx, |state, cx| state.focus(window, cx));
1662            window.dispatch_action(Box::new(Confirm { secondary: false }), cx);
1663        });
1664
1665        assert_eq!(
1666            state.read_with(cx, |state, _| state.selected_index()),
1667            Some(IndexPath::new(1).section(0))
1668        );
1669        assert_eq!(*confirmed.borrow(), Some(IndexPath::new(1).section(0)));
1670    }
1671
1672    #[gpui::test]
1673    fn initially_rendered_all_disabled_items_have_no_selected_index_and_ignore_enter(
1674        cx: &mut TestAppContext,
1675    ) {
1676        cx.update(crate::init);
1677        let confirmed = Rc::new(RefCell::new(None));
1678        let confirmed_for_render = confirmed.clone();
1679        let (harness, cx) = cx.add_window_view(move |window, cx| Harness {
1680            state: cx.new(|cx| CommandState::new(window, cx)),
1681            command: Rc::new(move |state| {
1682                let confirmed = confirmed_for_render.clone();
1683                Command::new(state)
1684                    .item(CommandItem::new().label("one").disabled(true))
1685                    .item(CommandItem::new().label("two").disabled(true))
1686                    .on_confirm(move |index_path, _, _| {
1687                        *confirmed.borrow_mut() = Some(index_path);
1688                    })
1689            }),
1690        });
1691        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1692
1693        cx.run_until_parked();
1694        cx.update(|window, cx| _ = window.draw(cx));
1695        cx.update(|window, cx| {
1696            state.update(cx, |state, cx| state.focus(window, cx));
1697            window.dispatch_action(Box::new(Confirm { secondary: false }), cx);
1698        });
1699
1700        assert_eq!(state.read_with(cx, |state, _| state.selected_index()), None);
1701        assert_eq!(*confirmed.borrow(), None);
1702    }
1703
1704    #[gpui::test]
1705    fn non_searchable_command_cancels_without_clearing_a_hidden_query(cx: &mut TestAppContext) {
1706        cx.update(crate::init);
1707        let cancelled = Rc::new(Cell::new(false));
1708        let cancelled_for_render = cancelled.clone();
1709        let query_calls = Rc::new(Cell::new(0));
1710        let query_calls_for_render = query_calls.clone();
1711        let (harness, cx) = cx.add_window_view(move |window, cx| Harness {
1712            state: cx.new(|cx| CommandState::new(window, cx)),
1713            command: Rc::new(move |state| {
1714                let cancelled = cancelled_for_render.clone();
1715                let query_calls = query_calls_for_render.clone();
1716                Command::new(state)
1717                    .searchable(false)
1718                    .item(CommandItem::new().label("alpha"))
1719                    .on_query(move |_, _, _| query_calls.set(query_calls.get() + 1))
1720                    .on_cancel(move |_, _| cancelled.set(true))
1721            }),
1722        });
1723        let state = cx.update(|_, cx| harness.read(cx).state.clone());
1724
1725        cx.run_until_parked();
1726        cx.update(|window, cx| _ = window.draw(cx));
1727        cx.update(|window, cx| {
1728            state.update(cx, |state, cx| {
1729                state.set_query("hidden query", window, cx);
1730                state.focus(window, cx);
1731            });
1732            window.dispatch_action(Box::new(Cancel), cx);
1733        });
1734
1735        assert!(cancelled.get());
1736        assert_eq!(query_calls.get(), 0);
1737        assert_eq!(
1738            state.read_with(cx, |state, cx| state.query(cx)),
1739            "hidden query"
1740        );
1741    }
1742
1743    #[gpui::test]
1744    fn moving_the_highlight_skips_disabled_items_and_wraps(cx: &mut TestAppContext) {
1745        cx.update(crate::init);
1746        let cx = cx.add_empty_window();
1747
1748        cx.update(|window, cx| {
1749            let state = cx.new(|cx| command_state(window, cx, suggestion_entries()));
1750
1751            state.update(cx, |state, cx| {
1752                state.update_matches(cx);
1753                state.reset_selection();
1754                assert_eq!(state.selected_index(), Some(IndexPath::new(0).section(0)));
1755
1756                state.select_by(1, window, cx);
1757                assert_eq!(state.selected_index(), Some(IndexPath::new(1).section(0)));
1758
1759                // "Calculator" is disabled, so it is stepped over.
1760                state.select_by(1, window, cx);
1761                assert_eq!(state.selected_index(), Some(IndexPath::new(0).section(1)));
1762
1763                state.select_by(-1, window, cx);
1764                assert_eq!(state.selected_index(), Some(IndexPath::new(1).section(0)));
1765
1766                // Wraps around the end, skipping the disabled item again.
1767                state.select_by(-1, window, cx);
1768                assert_eq!(state.selected_index(), Some(IndexPath::new(0).section(0)));
1769                state.select_by(-1, window, cx);
1770                assert_eq!(state.selected_index(), Some(IndexPath::new(1).section(1)));
1771            });
1772        });
1773    }
1774
1775    #[gpui::test]
1776    fn owner_can_set_and_clear_selection_by_original_index_path(cx: &mut TestAppContext) {
1777        cx.update(crate::init);
1778        let cx = cx.add_empty_window();
1779
1780        cx.update(|window, cx| {
1781            let initially_empty = cx.new(|cx| CommandState::new(window, cx));
1782            initially_empty.update(cx, |state, cx| {
1783                state.set_selected_index(None, window, cx);
1784                state.install_model(
1785                    CommandModel {
1786                        entries: suggestion_entries().into_iter().collect(),
1787                        ..CommandModel::default()
1788                    },
1789                    cx,
1790                );
1791                assert_eq!(state.selected_index(), None);
1792            });
1793
1794            let state = cx.new(|cx| command_state(window, cx, suggestion_entries()));
1795
1796            state.update(cx, |state, cx| {
1797                let target = IndexPath::new(1).section(1);
1798                state.set_selected_index(Some(target), window, cx);
1799                assert_eq!(state.selected_index(), Some(target));
1800
1801                state.set_selected_index(None, window, cx);
1802                assert_eq!(state.selected_index(), None);
1803
1804                state.install_model(
1805                    CommandModel {
1806                        entries: suggestion_entries().into_iter().collect(),
1807                        ..CommandModel::default()
1808                    },
1809                    cx,
1810                );
1811                assert_eq!(state.selected_index(), None);
1812
1813                state.set_query("calendar", window, cx);
1814                state.set_selected_index(Some(target), window, cx);
1815                assert_eq!(state.selected_index(), None);
1816            });
1817        });
1818    }
1819
1820    #[gpui::test]
1821    fn confirming_a_disabled_item_does_nothing(cx: &mut TestAppContext) {
1822        cx.update(crate::init);
1823        let cx = cx.add_empty_window();
1824
1825        cx.update(|window, cx| {
1826            let state = cx.new(|cx| {
1827                command_state(
1828                    window,
1829                    cx,
1830                    [
1831                        CommandEntry::Item(CommandItem::new().label("enabled")),
1832                        CommandEntry::Item(CommandItem::new().label("disabled").disabled(true)),
1833                    ],
1834                )
1835            });
1836
1837            state.update(cx, |state, cx| {
1838                state.update_matches(cx);
1839
1840                assert_eq!(state.matched_count(), 2);
1841                // Reaching the disabled row is only possible with the mouse or
1842                // an explicit index; confirming it must be a no-op.
1843                state.confirm(1, window, cx);
1844                assert_eq!(state.selected_index, Some(0));
1845            });
1846        });
1847    }
1848
1849    #[gpui::test]
1850    fn a_checked_item_uses_an_xsmall_trailing_check_icon(cx: &mut TestAppContext) {
1851        cx.update(crate::init);
1852        let cx = cx.add_empty_window();
1853        let unchecked_width = Rc::new(Cell::new(None));
1854        let checked_width = Rc::new(Cell::new(None));
1855        let (unchecked, checked) = cx.update(|window, cx| {
1856            let unchecked_state = cx.new(|cx| {
1857                command_state(
1858                    window,
1859                    cx,
1860                    [CommandEntry::Item(CommandItem::new().label("theme"))],
1861                )
1862            });
1863            let checked_state = cx.new(|cx| {
1864                command_state(
1865                    window,
1866                    cx,
1867                    [CommandEntry::Item(
1868                        CommandItem::new().label("theme").checked(true),
1869                    )],
1870                )
1871            });
1872            let unchecked_width = unchecked_width.clone();
1873            let checked_width = checked_width.clone();
1874            (
1875                cx.new(|_| CheckIconWidthHarness {
1876                    state: unchecked_state,
1877                    width: unchecked_width,
1878                }),
1879                cx.new(|_| CheckIconWidthHarness {
1880                    state: checked_state,
1881                    width: checked_width,
1882                }),
1883            )
1884        });
1885
1886        cx.draw(
1887            gpui::point(px(0.), px(0.)),
1888            gpui::AvailableSpace::min_size(),
1889            move |_, _| unchecked.into_any_element(),
1890        );
1891
1892        cx.draw(
1893            gpui::point(px(0.), px(0.)),
1894            gpui::AvailableSpace::min_size(),
1895            move |_, _| checked.into_any_element(),
1896        );
1897
1898        assert_eq!(
1899            checked_width.get().unwrap() - unchecked_width.get().unwrap(),
1900            px(20.)
1901        );
1902    }
1903
1904    struct CheckIconWidthHarness {
1905        state: Entity<CommandState>,
1906        width: Rc<Cell<Option<gpui::Pixels>>>,
1907    }
1908
1909    impl Render for CheckIconWidthHarness {
1910        fn render(
1911            &mut self,
1912            window: &mut Window,
1913            cx: &mut gpui::Context<Self>,
1914        ) -> impl IntoElement {
1915            let width = self.width.clone();
1916            let item = self.state.update(cx, |state, cx| {
1917                state.update_matches(cx);
1918                state.render_item(0, window, cx)
1919            });
1920
1921            div()
1922                .on_children_prepainted(move |bounds, _, _| width.set(Some(bounds[0].size.width)))
1923                .child(item)
1924        }
1925    }
1926
1927    struct Harness {
1928        state: Entity<CommandState>,
1929        command: Rc<dyn Fn(&Entity<CommandState>) -> Command>,
1930    }
1931
1932    impl Render for Harness {
1933        fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
1934            div()
1935                .size_full()
1936                .child((self.command)(&self.state).max_h(px(200.)))
1937        }
1938    }
1939
1940    #[gpui::test]
1941    fn header_and_footer_render_with_current_state(cx: &mut TestAppContext) {
1942        cx.update(crate::init);
1943        let header_calls = Rc::new(Cell::new(0));
1944        let footer_calls = Rc::new(Cell::new(0));
1945        let header_matched_count = Rc::new(Cell::new(None));
1946        let footer_matched_count = Rc::new(Cell::new(None));
1947
1948        let (harness, cx) = cx.add_window_view(|window, cx| HeaderFooterHarness {
1949            state: cx.new(|cx| CommandState::new(window, cx)),
1950            header_calls,
1951            footer_calls,
1952            header_matched_count,
1953            footer_matched_count,
1954        });
1955
1956        cx.run_until_parked();
1957        cx.update(|window, cx| _ = window.draw(cx));
1958
1959        let (header_calls, footer_calls, header_matched_count, footer_matched_count) =
1960            cx.update(|_, cx| {
1961                let harness = harness.read(cx);
1962                (
1963                    harness.header_calls.get(),
1964                    harness.footer_calls.get(),
1965                    harness.header_matched_count.get(),
1966                    harness.footer_matched_count.get(),
1967                )
1968            });
1969        assert!(header_calls > 0);
1970        assert!(footer_calls > 0);
1971        assert_eq!(header_matched_count, Some(2));
1972        assert_eq!(footer_matched_count, Some(2));
1973    }
1974
1975    #[gpui::test]
1976    fn custom_empty_slot_renders_with_current_state(cx: &mut TestAppContext) {
1977        cx.update(crate::init);
1978        let empty_calls = Rc::new(Cell::new(0));
1979        let empty_matched_count = Rc::new(Cell::new(None));
1980        let calls = empty_calls.clone();
1981        let matched_count = empty_matched_count.clone();
1982        let (_harness, cx) = cx.add_window_view(move |window, cx| Harness {
1983            state: cx.new(|cx| CommandState::new(window, cx)),
1984            command: Rc::new(move |state| {
1985                let calls = calls.clone();
1986                let matched_count = matched_count.clone();
1987                Command::new(state).empty(
1988                    move |state: &CommandState, _: &mut Window, _: &mut gpui::App| {
1989                        calls.set(calls.get() + 1);
1990                        matched_count.set(Some(state.matched_count()));
1991                        div().child("Custom empty")
1992                    },
1993                )
1994            }),
1995        });
1996
1997        cx.run_until_parked();
1998        cx.update(|window, cx| _ = window.draw(cx));
1999
2000        assert!(empty_calls.get() > 0);
2001        assert_eq!(empty_matched_count.get(), Some(0));
2002    }
2003
2004    fn entries_with_late_first_enabled_item() -> Vec<CommandEntry> {
2005        vec![
2006            CommandGroup::new()
2007                .label("Disabled")
2008                .items((0..30).map(|ix| {
2009                    CommandItem::new()
2010                        .label(format!("disabled-{ix}"))
2011                        .keywords(["match"])
2012                        .disabled(true)
2013                }))
2014                .into(),
2015            CommandEntry::Separator,
2016            CommandGroup::new()
2017                .label("Enabled")
2018                .item(CommandItem::new().label("enabled").keywords(["match"]))
2019                .into(),
2020        ]
2021    }
2022
2023    fn assert_first_enabled_row_is_scrolled_into_view(
2024        state: &Entity<CommandState>,
2025        cx: &mut TestAppContext,
2026    ) {
2027        let (selected_row, offset) = state.read_with(cx, |state, _| {
2028            (
2029                state.matched[state.selected_index.unwrap()].row_ix,
2030                state.scroll_handle.base_handle().offset().y,
2031            )
2032        });
2033
2034        assert!(selected_row > 30);
2035        assert!(
2036            offset < px(-900.),
2037            "the list should scroll to the selected row, not row zero ({offset:?})",
2038        );
2039    }
2040
2041    #[gpui::test]
2042    fn first_enabled_selection_resets_scroll_to_its_late_row(cx: &mut TestAppContext) {
2043        cx.update(crate::init);
2044        let (harness, cx) = cx.add_window_view(|window, cx| Harness {
2045            state: cx.new(|cx| CommandState::new(window, cx)),
2046            command: Rc::new(|state| {
2047                command_with_entries(state, entries_with_late_first_enabled_item())
2048            }),
2049        });
2050        let state = cx.update(|_, cx| harness.read(cx).state.clone());
2051
2052        cx.run_until_parked();
2053        cx.update(|window, cx| _ = window.draw(cx));
2054        assert_first_enabled_row_is_scrolled_into_view(&state, cx);
2055
2056        cx.update(|window, cx| {
2057            state.update(cx, |state, cx| state.set_query("match", window, cx));
2058            _ = window.draw(cx);
2059        });
2060        assert_first_enabled_row_is_scrolled_into_view(&state, cx);
2061
2062        cx.update(|window, cx| {
2063            harness.update(cx, |_, cx| {
2064                cx.notify();
2065            });
2066            _ = window.draw(cx);
2067        });
2068        assert_first_enabled_row_is_scrolled_into_view(&state, cx);
2069    }
2070
2071    struct HeaderFooterHarness {
2072        state: Entity<CommandState>,
2073        header_calls: Rc<Cell<usize>>,
2074        footer_calls: Rc<Cell<usize>>,
2075        header_matched_count: Rc<Cell<Option<usize>>>,
2076        footer_matched_count: Rc<Cell<Option<usize>>>,
2077    }
2078
2079    impl Render for HeaderFooterHarness {
2080        fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
2081            let header_calls = self.header_calls.clone();
2082            let header_matched_count = self.header_matched_count.clone();
2083            let footer_calls = self.footer_calls.clone();
2084            let footer_matched_count = self.footer_matched_count.clone();
2085
2086            div().size_full().child(
2087                Command::new(&self.state)
2088                    .items([
2089                        CommandItem::new().label("Calendar"),
2090                        CommandItem::new().label("Calculator"),
2091                    ])
2092                    .max_h(px(200.))
2093                    .header(move |state, _, _| {
2094                        header_calls.set(header_calls.get() + 1);
2095                        header_matched_count.set(Some(state.matched_count()));
2096                        div()
2097                    })
2098                    .footer(move |state, _, _| {
2099                        footer_calls.set(footer_calls.get() + 1);
2100                        footer_matched_count.set(Some(state.matched_count()));
2101                        div()
2102                    }),
2103            )
2104        }
2105    }
2106
2107    struct PaddedHarness {
2108        state: Entity<CommandState>,
2109    }
2110
2111    impl Render for PaddedHarness {
2112        fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
2113            div().size_full().child(
2114                Command::new(&self.state)
2115                    .item(
2116                        CommandItem::new()
2117                            .label("fixed")
2118                            .child(|_, _| div().h(px(32.))),
2119                    )
2120                    .max_h(px(200.))
2121                    .p_4(),
2122            )
2123        }
2124    }
2125
2126    struct WrappingHarness {
2127        state: Entity<CommandState>,
2128        width: Pixels,
2129        no_wrap: bool,
2130    }
2131
2132    impl Render for WrappingHarness {
2133        fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
2134            div().size_full().child(
2135                div().w(self.width).child(
2136                    Command::new(&self.state)
2137                        .item(CommandItem::new().label("wrapped").child(|_, _| {
2138                            div()
2139                                .w_full()
2140                                .child("A command row whose content wraps at narrow list widths")
2141                        }))
2142                        .max_h(px(200.))
2143                        .when(self.no_wrap, |this| this.whitespace_nowrap()),
2144                ),
2145            )
2146        }
2147    }
2148
2149    #[gpui::test]
2150    fn wrapping_rows_remeasure_for_the_list_content_width(cx: &mut TestAppContext) {
2151        cx.update(crate::init);
2152
2153        let (harness, cx) = cx.add_window_view(|window, cx| WrappingHarness {
2154            state: cx.new(|cx| CommandState::new(window, cx)),
2155            width: px(360.),
2156            no_wrap: false,
2157        });
2158
2159        cx.run_until_parked();
2160        cx.update(|window, cx| _ = window.draw(cx));
2161        cx.run_until_parked();
2162        cx.update(|window, cx| _ = window.draw(cx));
2163
2164        let wide = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes[0].height);
2165
2166        cx.update(|_, cx| {
2167            harness.update(cx, |harness, cx| {
2168                harness.width = px(120.);
2169                cx.notify();
2170            })
2171        });
2172        cx.run_until_parked();
2173        cx.update(|window, cx| _ = window.draw(cx));
2174        cx.run_until_parked();
2175        cx.update(|window, cx| _ = window.draw(cx));
2176        let narrow = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes[0].height);
2177
2178        assert!(
2179            narrow > wide,
2180            "the narrow list should cache a taller wrapped row ({narrow:?} vs {wide:?})",
2181        );
2182    }
2183
2184    #[gpui::test]
2185    fn wrapping_rows_remeasure_when_rem_size_changes(cx: &mut TestAppContext) {
2186        cx.update(crate::init);
2187
2188        let (harness, cx) = cx.add_window_view(|window, cx| {
2189            window.set_rem_size(px(20.));
2190            WrappingHarness {
2191                state: cx.new(|cx| CommandState::new(window, cx)),
2192                width: px(160.),
2193                no_wrap: false,
2194            }
2195        });
2196
2197        cx.run_until_parked();
2198        cx.update(|window, cx| _ = window.draw(cx));
2199        cx.run_until_parked();
2200        cx.update(|window, cx| _ = window.draw(cx));
2201        let smaller_rem = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes[0].height);
2202
2203        cx.update(|window, cx| {
2204            window.set_rem_size(px(28.));
2205            _ = window.draw(cx);
2206        });
2207        cx.run_until_parked();
2208        cx.update(|window, cx| _ = window.draw(cx));
2209        let larger_rem = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes[0].height);
2210
2211        assert!(
2212            larger_rem > smaller_rem,
2213            "a larger rem should remeasure the fixed-width wrapped row ({larger_rem:?} vs {smaller_rem:?})",
2214        );
2215    }
2216
2217    #[gpui::test]
2218    fn wrapping_rows_remeasure_when_inherited_typography_changes(cx: &mut TestAppContext) {
2219        cx.update(crate::init);
2220
2221        let (harness, cx) = cx.add_window_view(|window, cx| WrappingHarness {
2222            state: cx.new(|cx| CommandState::new(window, cx)),
2223            width: px(160.),
2224            no_wrap: false,
2225        });
2226
2227        cx.run_until_parked();
2228        cx.update(|window, cx| _ = window.draw(cx));
2229        cx.run_until_parked();
2230        cx.update(|window, cx| _ = window.draw(cx));
2231        let wrapped_height = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes[0].height);
2232
2233        cx.update(|window, cx| {
2234            harness.update(cx, |harness, cx| {
2235                harness.no_wrap = true;
2236                cx.notify();
2237            });
2238            _ = window.draw(cx);
2239        });
2240        cx.run_until_parked();
2241        cx.update(|window, cx| _ = window.draw(cx));
2242        let no_wrap_height = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes[0].height);
2243        assert!(
2244            no_wrap_height < wrapped_height,
2245            "a changed inherited typography should remeasure the fixed-width row ({no_wrap_height:?} vs {wrapped_height:?})",
2246        );
2247    }
2248
2249    #[gpui::test]
2250    fn outer_command_padding_does_not_inflate_measured_row_heights(cx: &mut TestAppContext) {
2251        cx.update(crate::init);
2252
2253        let (harness, cx) = cx.add_window_view(|window, cx| PaddedHarness {
2254            state: cx.new(|cx| CommandState::new(window, cx)),
2255        });
2256
2257        cx.run_until_parked();
2258        cx.update(|window, cx| _ = window.draw(cx));
2259        cx.run_until_parked();
2260        cx.update(|window, cx| _ = window.draw(cx));
2261        let height = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes[0].height);
2262
2263        assert_eq!(height, px(44.));
2264    }
2265
2266    #[gpui::test]
2267    fn custom_rows_keep_independent_heights(cx: &mut TestAppContext) {
2268        cx.update(crate::init);
2269
2270        let (harness, cx) = cx.add_window_view(|window, cx| Harness {
2271            state: cx.new(|cx| CommandState::new(window, cx)),
2272            command: Rc::new(|state| {
2273                Command::new(state)
2274                    .group(
2275                        CommandGroup::new().label("Short").item(
2276                            CommandItem::new()
2277                                .label("short")
2278                                .child(|_, _| div().h(px(32.))),
2279                        ),
2280                    )
2281                    .separator()
2282                    .group(
2283                        CommandGroup::new().label("Tall").item(
2284                            CommandItem::new()
2285                                .label("tall")
2286                                .child(|_, _| div().h(px(72.))),
2287                        ),
2288                    )
2289            }),
2290        });
2291
2292        cx.run_until_parked();
2293        cx.update(|window, cx| _ = window.draw(cx));
2294        let row_sizes = cx.update(|_, cx| harness.read(cx).state.read(cx).row_sizes.clone());
2295
2296        assert_eq!(row_sizes.len(), 5);
2297        assert!(row_sizes[0].height > px(0.));
2298        assert_eq!(row_sizes[1].height, px(44.));
2299        assert_eq!(row_sizes[2].height, px(SEPARATOR_ROW_HEIGHT));
2300        assert!(row_sizes[3].height > px(0.));
2301        assert_eq!(row_sizes[4].height, px(84.));
2302    }
2303
2304    #[gpui::test]
2305    fn reinstalling_a_model_preserves_selection_by_index_path_and_remeasures_rows(
2306        cx: &mut TestAppContext,
2307    ) {
2308        cx.update(crate::init);
2309        let reversed = Rc::new(Cell::new(false));
2310        let reversed_for_render = reversed.clone();
2311        let (harness, cx) = cx.add_window_view(|window, cx| Harness {
2312            state: cx.new(|cx| CommandState::new(window, cx)),
2313            command: Rc::new(move |state| {
2314                if reversed_for_render.get() {
2315                    Command::new(state)
2316                        .item(
2317                            CommandItem::new()
2318                                .label("beta")
2319                                .child(|_, _| div().h(px(72.))),
2320                        )
2321                        .item(
2322                            CommandItem::new()
2323                                .label("alpha")
2324                                .child(|_, _| div().h(px(32.))),
2325                        )
2326                } else {
2327                    Command::new(state)
2328                        .item(
2329                            CommandItem::new()
2330                                .label("alpha")
2331                                .child(|_, _| div().h(px(32.))),
2332                        )
2333                        .item(
2334                            CommandItem::new()
2335                                .label("beta")
2336                                .child(|_, _| div().h(px(72.))),
2337                        )
2338                }
2339            }),
2340        });
2341        let state = cx.update(|_, cx| harness.read(cx).state.clone());
2342
2343        cx.run_until_parked();
2344        cx.update(|window, cx| _ = window.draw(cx));
2345        cx.update(|window, cx| {
2346            state.update(cx, |state, cx| state.select_by(1, window, cx));
2347        });
2348        assert_eq!(
2349            state.read_with(cx, |state, _| state.selected_index()),
2350            Some(IndexPath::new(1).section(0)),
2351        );
2352
2353        reversed.set(true);
2354        cx.update(|window, cx| {
2355            harness.update(cx, |_, cx| cx.notify());
2356            _ = window.draw(cx);
2357        });
2358
2359        let (selected_matched_index, selected_index, row_sizes) =
2360            state.read_with(cx, |state, _| {
2361                (
2362                    state.selected_index,
2363                    state.selected_index(),
2364                    state.row_sizes.clone(),
2365                )
2366            });
2367        assert_eq!(selected_matched_index, Some(1));
2368        assert_eq!(selected_index, Some(IndexPath::new(1).section(0)));
2369        assert_eq!(row_sizes[0].height, px(84.));
2370        assert_eq!(row_sizes[1].height, px(44.));
2371    }
2372
2373    #[gpui::test]
2374    fn a_state_redraw_reuses_the_installed_custom_row_measurement(cx: &mut TestAppContext) {
2375        cx.update(crate::init);
2376        let renders = Rc::new(Cell::new(0));
2377        let count = renders.clone();
2378        let cx = cx.add_empty_window();
2379        let state = cx.update(|window, cx| {
2380            cx.new(|cx| {
2381                command_state(
2382                    window,
2383                    cx,
2384                    [CommandEntry::Item(
2385                        CommandItem::new().label("custom").child(move |_, _| {
2386                            count.set(count.get() + 1);
2387                            div().child("Custom")
2388                        }),
2389                    )],
2390                )
2391            })
2392        });
2393
2394        let first_state = state.clone();
2395        cx.draw(
2396            gpui::point(px(0.), px(0.)),
2397            gpui::AvailableSpace::min_size(),
2398            move |_, _| first_state.into_any_element(),
2399        );
2400        let settled_state = state.clone();
2401        cx.draw(
2402            gpui::point(px(0.), px(0.)),
2403            gpui::AvailableSpace::min_size(),
2404            move |_, _| settled_state.into_any_element(),
2405        );
2406        let after_first_draw = renders.get();
2407        cx.draw(
2408            gpui::point(px(0.), px(0.)),
2409            gpui::AvailableSpace::min_size(),
2410            move |_, _| state.into_any_element(),
2411        );
2412
2413        assert_eq!(renders.get() - after_first_draw, 2);
2414    }
2415
2416    #[gpui::test]
2417    fn moving_past_the_visible_rows_scrolls_the_list(cx: &mut TestAppContext) {
2418        cx.update(crate::init);
2419
2420        let (harness, cx) = cx.add_window_view(|window, cx| Harness {
2421            state: cx.new(|cx| CommandState::new(window, cx)),
2422            command: Rc::new(|state| {
2423                Command::new(state)
2424                    .items((0..50).map(|ix| CommandItem::new().label(format!("Item {ix}"))))
2425            }),
2426        });
2427
2428        cx.run_until_parked();
2429        cx.update(|window, cx| _ = window.draw(cx));
2430
2431        let state = cx.update(|_, cx| harness.read(cx).state.clone());
2432        assert_eq!(
2433            state.read_with(cx, |state, _| state.scroll_handle.base_handle().offset().y),
2434            px(0.),
2435        );
2436
2437        // The list is capped well below 50 rows, so walking to the last one has
2438        // to bring the viewport with it.
2439        cx.update(|window, cx| {
2440            state.update(cx, |state, cx| {
2441                for _ in 0..49 {
2442                    state.select_by(1, window, cx);
2443                }
2444            })
2445        });
2446        cx.update(|window, cx| _ = window.draw(cx));
2447
2448        assert_eq!(
2449            state.read_with(cx, |state, _| state.selected_index()),
2450            Some(IndexPath::new(49).section(0))
2451        );
2452        assert!(
2453            state.read_with(cx, |state, _| state.scroll_handle.base_handle().offset().y) < px(0.),
2454            "selecting the last row should have scrolled the list",
2455        );
2456    }
2457
2458    #[gpui::test]
2459    fn a_reinstalled_model_does_not_scroll_a_preserved_selection(cx: &mut TestAppContext) {
2460        cx.update(crate::init);
2461
2462        let (harness, cx) = cx.add_window_view(|window, cx| Harness {
2463            state: cx.new(|cx| CommandState::new(window, cx)),
2464            command: Rc::new(|state| {
2465                Command::new(state)
2466                    .items((0..50).map(|ix| CommandItem::new().label(format!("Item {ix}"))))
2467            }),
2468        });
2469
2470        cx.run_until_parked();
2471        cx.update(|window, cx| _ = window.draw(cx));
2472
2473        let state = cx.update(|_, cx| harness.read(cx).state.clone());
2474
2475        // Hover selection does not scroll, and the host re-render it notifies
2476        // reinstalls the model with the selection preserved. That reinstall
2477        // must not scroll either, or the hover still moves the list one frame
2478        // later.
2479        cx.update(|window, cx| {
2480            state.update(cx, |state, cx| state.select(10, window, cx));
2481        });
2482        cx.update(|window, cx| _ = window.draw(cx));
2483
2484        assert_eq!(
2485            state.read_with(cx, |state, _| state.scroll_handle.base_handle().offset().y),
2486            px(0.),
2487            "reinstalling the model must keep the scroll position",
2488        );
2489    }
2490}