Refine command palette (#3311)

Nate Butler created

[[PR Description]]

Update command palette styles

Release Notes:

- N/A

Change summary

crates/command_palette2/src/command_palette.rs | 25 +++++--
crates/picker2/src/picker2.rs                  | 60 ++++++++++++-------
crates/storybook2/src/stories/picker.rs        |  4 +
3 files changed, 59 insertions(+), 30 deletions(-)

Detailed changes

crates/command_palette2/src/command_palette.rs 🔗

@@ -6,9 +6,12 @@ use gpui::{
     WeakView, WindowContext,
 };
 use picker::{Picker, PickerDelegate};
-use std::cmp::{self, Reverse};
+use std::{
+    cmp::{self, Reverse},
+    sync::Arc,
+};
 use theme::ActiveTheme;
-use ui::{v_stack, Label, StyledExt};
+use ui::{v_stack, HighlightedLabel, StyledExt};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
@@ -147,6 +150,10 @@ impl CommandPaletteDelegate {
 impl PickerDelegate for CommandPaletteDelegate {
     type ListItem = Div<Picker<Self>>;
 
+    fn placeholder_text(&self) -> Arc<str> {
+        "Execute a command...".into()
+    }
+
     fn match_count(&self) -> usize {
         self.matches.len()
     }
@@ -296,11 +303,10 @@ impl PickerDelegate for CommandPaletteDelegate {
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Self::ListItem {
         let colors = cx.theme().colors();
-        let Some(command) = self
-            .matches
-            .get(ix)
-            .and_then(|m| self.commands.get(m.candidate_id))
-        else {
+        let Some(r#match) = self.matches.get(ix) else {
+            return div();
+        };
+        let Some(command) = self.commands.get(r#match.candidate_id) else {
             return div();
         };
 
@@ -312,7 +318,10 @@ impl PickerDelegate for CommandPaletteDelegate {
             .rounded_md()
             .when(selected, |this| this.bg(colors.ghost_element_selected))
             .hover(|this| this.bg(colors.ghost_element_hover))
-            .child(Label::new(command.name.clone()))
+            .child(HighlightedLabel::new(
+                command.name.clone(),
+                r#match.positions.clone(),
+            ))
     }
 
     // fn render_match(

crates/picker2/src/picker2.rs 🔗

@@ -4,8 +4,8 @@ use gpui::{
     StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
     WindowContext,
 };
-use std::cmp;
-use ui::{prelude::*, v_stack, Divider};
+use std::{cmp, sync::Arc};
+use ui::{prelude::*, v_stack, Divider, Label, LabelColor};
 
 pub struct Picker<D: PickerDelegate> {
     pub delegate: D,
@@ -21,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static {
     fn selected_index(&self) -> usize;
     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
 
-    // fn placeholder_text(&self) -> Arc<str>;
+    fn placeholder_text(&self) -> Arc<str>;
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
 
     fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
@@ -37,7 +37,11 @@ pub trait PickerDelegate: Sized + 'static {
 
 impl<D: PickerDelegate> Picker<D> {
     pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
-        let editor = cx.build_view(|cx| Editor::single_line(cx));
+        let editor = cx.build_view(|cx| {
+            let mut editor = Editor::single_line(cx);
+            editor.set_placeholder_text(delegate.placeholder_text(), cx);
+            editor
+        });
         cx.subscribe(&editor, Self::on_input_editor_event).detach();
         Self {
             delegate,
@@ -159,23 +163,35 @@ impl<D: PickerDelegate> Render for Picker<D> {
                     .child(div().px_1().py_0p5().child(self.editor.clone())),
             )
             .child(Divider::horizontal())
-            .child(
-                v_stack()
-                    .p_1()
-                    .grow()
-                    .child(
-                        uniform_list("candidates", self.delegate.match_count(), {
-                            move |this: &mut Self, visible_range, cx| {
-                                let selected_ix = this.delegate.selected_index();
-                                visible_range
-                                    .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx))
-                                    .collect()
-                            }
-                        })
-                        .track_scroll(self.scroll_handle.clone()),
-                    )
-                    .max_h_72()
-                    .overflow_hidden(),
-            )
+            .when(self.delegate.match_count() > 0, |el| {
+                el.child(
+                    v_stack()
+                        .p_1()
+                        .grow()
+                        .child(
+                            uniform_list("candidates", self.delegate.match_count(), {
+                                move |this: &mut Self, visible_range, cx| {
+                                    let selected_ix = this.delegate.selected_index();
+                                    visible_range
+                                        .map(|ix| {
+                                            this.delegate.render_match(ix, ix == selected_ix, cx)
+                                        })
+                                        .collect()
+                                }
+                            })
+                            .track_scroll(self.scroll_handle.clone()),
+                        )
+                        .max_h_72()
+                        .overflow_hidden(),
+                )
+            })
+            .when(self.delegate.match_count() == 0, |el| {
+                el.child(
+                    v_stack()
+                        .p_1()
+                        .grow()
+                        .child(Label::new("No matches").color(LabelColor::Muted)),
+                )
+            })
     }
 }

crates/storybook2/src/stories/picker.rs 🔗

@@ -44,6 +44,10 @@ impl PickerDelegate for Delegate {
         self.candidates.len()
     }
 
+    fn placeholder_text(&self) -> Arc<str> {
+        "Test".into()
+    }
+
     fn render_match(
         &self,
         ix: usize,