Start work on adding a filter editor to the picker

Max Brunsfeld , Mikayla , and Marshall created

Implement picker as a view instead of as a component

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>

Change summary

Cargo.lock                              |   2 
crates/picker2/src/picker2.rs           | 212 +++++++++++---------------
crates/storybook2/Cargo.toml            |   2 
crates/storybook2/src/stories/picker.rs | 152 +++++++++---------
crates/storybook2/src/storybook2.rs     |   2 
5 files changed, 173 insertions(+), 197 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8557,8 +8557,10 @@ dependencies = [
  "backtrace-on-stack-overflow",
  "chrono",
  "clap 4.4.4",
+ "editor2",
  "gpui2",
  "itertools 0.11.0",
+ "language2",
  "log",
  "menu2",
  "picker2",

crates/picker2/src/picker2.rs 🔗

@@ -1,149 +1,121 @@
+use editor::Editor;
 use gpui::{
-    div, uniform_list, Component, ElementId, FocusHandle, ParentElement, StatelessInteractive,
-    Styled, UniformListScrollHandle, ViewContext,
+    div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled,
+    UniformListScrollHandle, View, ViewContext, VisualContext,
 };
 use std::cmp;
 
-#[derive(Component)]
-pub struct Picker<V: PickerDelegate> {
-    id: ElementId,
-    focus_handle: FocusHandle,
-    phantom: std::marker::PhantomData<V>,
+pub struct Picker<D: PickerDelegate> {
+    pub delegate: D,
+    scroll_handle: UniformListScrollHandle,
+    editor: View<Editor>,
 }
 
 pub trait PickerDelegate: Sized + 'static {
-    type ListItem: Component<Self>;
+    type ListItem: Component<Picker<Self>>;
 
-    fn match_count(&self, picker_id: ElementId) -> usize;
-    fn selected_index(&self, picker_id: ElementId) -> usize;
-    fn set_selected_index(&mut self, ix: usize, picker_id: ElementId, cx: &mut ViewContext<Self>);
+    fn match_count(&self) -> usize;
+    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 update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
 
-    fn confirm(&mut self, secondary: bool, picker_id: ElementId, cx: &mut ViewContext<Self>);
-    fn dismissed(&mut self, picker_id: ElementId, cx: &mut ViewContext<Self>);
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
 
     fn render_match(
         &self,
         ix: usize,
         selected: bool,
-        picker_id: ElementId,
-        cx: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Picker<Self>>,
     ) -> Self::ListItem;
 }
 
-impl<V: PickerDelegate> Picker<V> {
-    pub fn new(id: impl Into<ElementId>, focus_handle: FocusHandle) -> Self {
+impl<D: PickerDelegate> Picker<D> {
+    pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
         Self {
-            id: id.into(),
-            focus_handle,
-            phantom: std::marker::PhantomData,
+            delegate,
+            scroll_handle: UniformListScrollHandle::new(),
+            editor: cx.build_view(|cx| Editor::single_line(cx)),
         }
     }
 
-    fn bind_actions<T: StatelessInteractive<V>>(
-        div: T,
-        id: ElementId,
-        scroll_handle: &UniformListScrollHandle,
-    ) -> T {
-        div.on_action({
-            let id = id.clone();
-            let scroll_handle = scroll_handle.clone();
-            move |view: &mut V, _: &menu::SelectNext, cx| {
-                let count = view.match_count(id.clone());
-                if count > 0 {
-                    let index = view.selected_index(id.clone());
-                    let ix = cmp::min(index + 1, count - 1);
-                    view.set_selected_index(ix, id.clone(), cx);
-                    scroll_handle.scroll_to_item(ix);
-                }
-            }
-        })
-        .on_action({
-            let id = id.clone();
-            let scroll_handle = scroll_handle.clone();
-            move |view, _: &menu::SelectPrev, cx| {
-                let count = view.match_count(id.clone());
-                if count > 0 {
-                    let index = view.selected_index(id.clone());
-                    let ix = index.saturating_sub(1);
-                    view.set_selected_index(ix, id.clone(), cx);
-                    scroll_handle.scroll_to_item(ix);
-                }
-            }
-        })
-        .on_action({
-            let id = id.clone();
-            let scroll_handle = scroll_handle.clone();
-            move |view: &mut V, _: &menu::SelectFirst, cx| {
-                let count = view.match_count(id.clone());
-                if count > 0 {
-                    view.set_selected_index(0, id.clone(), cx);
-                    scroll_handle.scroll_to_item(0);
-                }
-            }
-        })
-        .on_action({
-            let id = id.clone();
-            let scroll_handle = scroll_handle.clone();
-            move |view: &mut V, _: &menu::SelectLast, cx| {
-                let count = view.match_count(id.clone());
-                if count > 0 {
-                    view.set_selected_index(count - 1, id.clone(), cx);
-                    scroll_handle.scroll_to_item(count - 1);
-                }
-            }
-        })
-        .on_action({
-            let id = id.clone();
-            move |view: &mut V, _: &menu::Cancel, cx| {
-                view.dismissed(id.clone(), cx);
-            }
-        })
-        .on_action({
-            let id = id.clone();
-            move |view: &mut V, _: &menu::Confirm, cx| {
-                view.confirm(false, id.clone(), cx);
-            }
-        })
-        .on_action({
-            let id = id.clone();
-            move |view: &mut V, _: &menu::SecondaryConfirm, cx| {
-                view.confirm(true, id.clone(), cx);
-            }
-        })
+    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        if count > 0 {
+            let index = self.delegate.selected_index();
+            let ix = cmp::min(index + 1, count - 1);
+            self.delegate.set_selected_index(ix, cx);
+            self.scroll_handle.scroll_to_item(ix);
+        }
+    }
+
+    fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        if count > 0 {
+            let index = self.delegate.selected_index();
+            let ix = index.saturating_sub(1);
+            self.delegate.set_selected_index(ix, cx);
+            self.scroll_handle.scroll_to_item(ix);
+        }
+    }
+
+    fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        if count > 0 {
+            self.delegate.set_selected_index(0, cx);
+            self.scroll_handle.scroll_to_item(0);
+        }
+    }
+
+    fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        if count > 0 {
+            self.delegate.set_selected_index(count - 1, cx);
+            self.scroll_handle.scroll_to_item(count - 1);
+        }
+    }
+
+    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        self.delegate.dismissed(cx);
+    }
+
+    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+        self.delegate.confirm(false, cx);
+    }
+
+    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        self.delegate.confirm(true, cx);
     }
 }
 
-impl<V: 'static + PickerDelegate> Picker<V> {
-    pub fn render(self, view: &mut V, _cx: &mut ViewContext<V>) -> impl Component<V> {
-        let id = self.id.clone();
-        let scroll_handle = UniformListScrollHandle::new();
-        Self::bind_actions(
-            div()
-                .id(self.id.clone())
-                .size_full()
-                .track_focus(&self.focus_handle)
-                .context("picker")
-                .child(
-                    uniform_list(
-                        "candidates",
-                        view.match_count(self.id.clone()),
-                        move |view: &mut V, visible_range, cx| {
-                            let selected_ix = view.selected_index(self.id.clone());
-                            visible_range
-                                .map(|ix| {
-                                    view.render_match(ix, ix == selected_ix, self.id.clone(), cx)
-                                })
-                                .collect()
-                        },
-                    )
-                    .track_scroll(scroll_handle.clone())
-                    .size_full(),
-                ),
-            id,
-            &scroll_handle,
-        )
+impl<D: PickerDelegate> Render for Picker<D> {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .size_full()
+            .context("picker")
+            .on_action(Self::select_next)
+            .on_action(Self::select_prev)
+            .on_action(Self::select_first)
+            .on_action(Self::select_last)
+            .on_action(Self::cancel)
+            .on_action(Self::confirm)
+            .on_action(Self::secondary_confirm)
+            .child(self.editor.clone())
+            .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())
+                .size_full(),
+            )
     }
 }

crates/storybook2/Cargo.toml 🔗

@@ -13,9 +13,11 @@ anyhow.workspace = true
 # TODO: Remove after diagnosing stack overflow.
 backtrace-on-stack-overflow = "0.3.0"
 clap = { version = "4.4", features = ["derive", "string"] }
+editor = { package = "editor2", path = "../editor2" }
 chrono = "0.4"
 gpui = { package = "gpui2", path = "../gpui2" }
 itertools = "0.11.0"
+language = { package = "language2", path = "../language2" }
 log.workspace = true
 rust-embed.workspace = true
 serde.workspace = true

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

@@ -6,15 +6,19 @@ use picker::{Picker, PickerDelegate};
 use theme2::ActiveTheme;
 
 pub struct PickerStory {
-    selected_ix: usize,
-    candidates: Vec<SharedString>,
+    picker: View<Picker<Delegate>>,
     focus_handle: FocusHandle,
 }
 
-impl PickerDelegate for PickerStory {
-    type ListItem = Div<Self>;
+struct Delegate {
+    candidates: Vec<SharedString>,
+    selected_ix: usize,
+}
+
+impl PickerDelegate for Delegate {
+    type ListItem = Div<Picker<Self>>;
 
-    fn match_count(&self, _picker_id: gpui::ElementId) -> usize {
+    fn match_count(&self) -> usize {
         self.candidates.len()
     }
 
@@ -22,8 +26,7 @@ impl PickerDelegate for PickerStory {
         &self,
         ix: usize,
         selected: bool,
-        _picker_id: gpui::ElementId,
-        cx: &mut gpui::ViewContext<Self>,
+        cx: &mut gpui::ViewContext<Picker<Self>>,
     ) -> Self::ListItem {
         let colors = cx.theme().colors();
 
@@ -40,26 +43,16 @@ impl PickerDelegate for PickerStory {
             .child(self.candidates[ix].clone())
     }
 
-    fn selected_index(&self, picker_id: gpui::ElementId) -> usize {
+    fn selected_index(&self) -> usize {
         self.selected_ix
     }
 
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _picker_id: gpui::ElementId,
-        cx: &mut gpui::ViewContext<Self>,
-    ) {
+    fn set_selected_index(&mut self, ix: usize, cx: &mut gpui::ViewContext<Picker<Self>>) {
         self.selected_ix = ix;
         cx.notify();
     }
 
-    fn confirm(
-        &mut self,
-        secondary: bool,
-        picker_id: gpui::ElementId,
-        cx: &mut gpui::ViewContext<Self>,
-    ) {
+    fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext<Picker<Self>>) {
         if secondary {
             eprintln!("Secondary confirmed {}", self.candidates[self.selected_ix])
         } else {
@@ -67,7 +60,7 @@ impl PickerDelegate for PickerStory {
         }
     }
 
-    fn dismissed(&mut self, picker_id: gpui::ElementId, cx: &mut gpui::ViewContext<Self>) {
+    fn dismissed(&mut self, cx: &mut gpui::ViewContext<Picker<Self>>) {
         cx.quit();
     }
 }
@@ -98,58 +91,65 @@ impl PickerStory {
 
             PickerStory {
                 focus_handle,
-                candidates: vec![
-                    "Baguette (France)".into(),
-                    "Baklava (Turkey)".into(),
-                    "Beef Wellington (UK)".into(),
-                    "Biryani (India)".into(),
-                    "Borscht (Ukraine)".into(),
-                    "Bratwurst (Germany)".into(),
-                    "Bulgogi (Korea)".into(),
-                    "Burrito (USA)".into(),
-                    "Ceviche (Peru)".into(),
-                    "Chicken Tikka Masala (India)".into(),
-                    "Churrasco (Brazil)".into(),
-                    "Couscous (North Africa)".into(),
-                    "Croissant (France)".into(),
-                    "Dim Sum (China)".into(),
-                    "Empanada (Argentina)".into(),
-                    "Fajitas (Mexico)".into(),
-                    "Falafel (Middle East)".into(),
-                    "Feijoada (Brazil)".into(),
-                    "Fish and Chips (UK)".into(),
-                    "Fondue (Switzerland)".into(),
-                    "Goulash (Hungary)".into(),
-                    "Haggis (Scotland)".into(),
-                    "Kebab (Middle East)".into(),
-                    "Kimchi (Korea)".into(),
-                    "Lasagna (Italy)".into(),
-                    "Maple Syrup Pancakes (Canada)".into(),
-                    "Moussaka (Greece)".into(),
-                    "Pad Thai (Thailand)".into(),
-                    "Paella (Spain)".into(),
-                    "Pancakes (USA)".into(),
-                    "Pasta Carbonara (Italy)".into(),
-                    "Pavlova (Australia)".into(),
-                    "Peking Duck (China)".into(),
-                    "Pho (Vietnam)".into(),
-                    "Pierogi (Poland)".into(),
-                    "Pizza (Italy)".into(),
-                    "Poutine (Canada)".into(),
-                    "Pretzel (Germany)".into(),
-                    "Ramen (Japan)".into(),
-                    "Rendang (Indonesia)".into(),
-                    "Sashimi (Japan)".into(),
-                    "Satay (Indonesia)".into(),
-                    "Shepherd's Pie (Ireland)".into(),
-                    "Sushi (Japan)".into(),
-                    "Tacos (Mexico)".into(),
-                    "Tandoori Chicken (India)".into(),
-                    "Tortilla (Spain)".into(),
-                    "Tzatziki (Greece)".into(),
-                    "Wiener Schnitzel (Austria)".into(),
-                ],
-                selected_ix: 0,
+                picker: cx.build_view(|cx| {
+                    Picker::new(
+                        Delegate {
+                            candidates: vec![
+                                "Baguette (France)".into(),
+                                "Baklava (Turkey)".into(),
+                                "Beef Wellington (UK)".into(),
+                                "Biryani (India)".into(),
+                                "Borscht (Ukraine)".into(),
+                                "Bratwurst (Germany)".into(),
+                                "Bulgogi (Korea)".into(),
+                                "Burrito (USA)".into(),
+                                "Ceviche (Peru)".into(),
+                                "Chicken Tikka Masala (India)".into(),
+                                "Churrasco (Brazil)".into(),
+                                "Couscous (North Africa)".into(),
+                                "Croissant (France)".into(),
+                                "Dim Sum (China)".into(),
+                                "Empanada (Argentina)".into(),
+                                "Fajitas (Mexico)".into(),
+                                "Falafel (Middle East)".into(),
+                                "Feijoada (Brazil)".into(),
+                                "Fish and Chips (UK)".into(),
+                                "Fondue (Switzerland)".into(),
+                                "Goulash (Hungary)".into(),
+                                "Haggis (Scotland)".into(),
+                                "Kebab (Middle East)".into(),
+                                "Kimchi (Korea)".into(),
+                                "Lasagna (Italy)".into(),
+                                "Maple Syrup Pancakes (Canada)".into(),
+                                "Moussaka (Greece)".into(),
+                                "Pad Thai (Thailand)".into(),
+                                "Paella (Spain)".into(),
+                                "Pancakes (USA)".into(),
+                                "Pasta Carbonara (Italy)".into(),
+                                "Pavlova (Australia)".into(),
+                                "Peking Duck (China)".into(),
+                                "Pho (Vietnam)".into(),
+                                "Pierogi (Poland)".into(),
+                                "Pizza (Italy)".into(),
+                                "Poutine (Canada)".into(),
+                                "Pretzel (Germany)".into(),
+                                "Ramen (Japan)".into(),
+                                "Rendang (Indonesia)".into(),
+                                "Sashimi (Japan)".into(),
+                                "Satay (Indonesia)".into(),
+                                "Shepherd's Pie (Ireland)".into(),
+                                "Sushi (Japan)".into(),
+                                "Tacos (Mexico)".into(),
+                                "Tandoori Chicken (India)".into(),
+                                "Tortilla (Spain)".into(),
+                                "Tzatziki (Greece)".into(),
+                                "Wiener Schnitzel (Austria)".into(),
+                            ],
+                            selected_ix: 0,
+                        },
+                        cx,
+                    )
+                }),
             }
         })
     }
@@ -159,11 +159,9 @@ impl Render for PickerStory {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-        let theme = cx.theme();
-
         div()
-            .bg(theme.styles.colors.background)
+            .bg(cx.theme().styles.colors.background)
             .size_full()
-            .child(Picker::new("picker_story", self.focus_handle.clone()))
+            .child(self.picker.clone())
     }
 }

crates/storybook2/src/storybook2.rs 🔗

@@ -72,6 +72,8 @@ fn main() {
         ThemeSettings::override_global(theme_settings, cx);
 
         ui::settings::init(cx);
+        language::init(cx);
+        editor::init(cx);
 
         let window = cx.open_window(
             WindowOptions {