Finish converting all the pickers to the new API

Antonio Scandurra created

Change summary

crates/command_palette/src/command_palette.rs     |  22 +
crates/file_finder/src/file_finder.rs             | 108 ++++++--
crates/go_to_line/src/go_to_line.rs               |  25 -
crates/language_selector/src/language_selector.rs | 121 +++------
crates/outline/src/outline.rs                     | 130 +++-------
crates/picker/src/picker.rs                       |  32 +-
crates/project_symbols/src/project_symbols.rs     | 199 +++++++---------
crates/recent_projects/src/recent_projects.rs     | 152 +++++-------
crates/theme_selector/src/theme_selector.rs       | 161 ++++--------
crates/welcome/src/base_keymap_picker.rs          | 111 +++------
crates/zed/src/main.rs                            |   2 
11 files changed, 444 insertions(+), 619 deletions(-)

Detailed changes

crates/command_palette/src/command_palette.rs 🔗

@@ -12,7 +12,7 @@ use workspace::Workspace;
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(toggle_command_palette);
-    Picker::<CommandPaletteDelegate>::init(cx);
+    CommandPalette::init(cx);
 }
 
 actions!(command_palette, [Toggle]);
@@ -155,9 +155,7 @@ impl PickerDelegate for CommandPaletteDelegate {
         })
     }
 
-    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
-    }
+    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
 
     fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         if !self.matches.is_empty() {
@@ -330,8 +328,8 @@ mod tests {
             .await;
 
         palette.update(cx, |palette, cx| {
-            assert_eq!(palette.matches[0].string, "editor: backspace");
-            palette.confirm(cx);
+            assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
+            palette.confirm(&Default::default(), cx);
         });
 
         editor.read_with(cx, |editor, cx| {
@@ -346,20 +344,24 @@ mod tests {
         });
 
         workspace.update(cx, |workspace, cx| {
-            CommandPaletteDelegate::toggle(workspace, &Toggle, cx);
+            toggle_command_palette(workspace, &Toggle, cx);
         });
 
         // Assert editor command not present
         let palette = workspace.read_with(cx, |workspace, _| {
-            workspace.modal::<CommandPaletteDelegate>().unwrap()
+            workspace.modal::<CommandPalette>().unwrap()
         });
 
         palette
             .update(cx, |palette, cx| {
-                palette.update_matches("bcksp".to_string(), cx)
+                palette
+                    .delegate_mut()
+                    .update_matches("bcksp".to_string(), cx)
             })
             .await;
 
-        palette.update(cx, |palette, _| assert!(palette.matches.is_empty()));
+        palette.update(cx, |palette, _| {
+            assert!(palette.delegate().matches.is_empty())
+        });
     }
 }

crates/file_finder/src/file_finder.rs 🔗

@@ -94,8 +94,7 @@ impl FileFinderDelegate {
         cx: &mut ViewContext<FileFinder>,
     ) -> Self {
         cx.observe(&project, |picker, _, cx| {
-            let query = picker.query(cx);
-            picker.delegate_mut().spawn_search(query, cx).detach();
+            picker.update_matches(picker.query(cx), cx);
         })
         .detach();
         Self {
@@ -366,16 +365,21 @@ mod tests {
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) = cx.add_window(|cx| {
             Picker::new(
-                FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx),
+                FileFinderDelegate::new(
+                    workspace.downgrade(),
+                    workspace.read(cx).project().clone(),
+                    None,
+                    cx,
+                ),
                 cx,
             )
         });
 
         let query = "hi".to_string();
         finder
-            .update(cx, |f, cx| f.spawn_search(query.clone(), cx))
+            .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
             .await;
-        finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5));
+        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5));
 
         finder.update(cx, |finder, cx| {
             let delegate = finder.delegate_mut();
@@ -385,7 +389,7 @@ mod tests {
             // returning only a subset of the matches that would have been found.
             drop(delegate.spawn_search(query.clone(), cx));
             delegate.set_matches(
-                finder.delegate().latest_search_id,
+                delegate.latest_search_id,
                 true, // did-cancel
                 query.clone(),
                 vec![matches[1].clone(), matches[3].clone()],
@@ -445,14 +449,19 @@ mod tests {
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) = cx.add_window(|cx| {
             Picker::new(
-                FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx),
+                FileFinderDelegate::new(
+                    workspace.downgrade(),
+                    workspace.read(cx).project().clone(),
+                    None,
+                    cx,
+                ),
                 cx,
             )
         });
         finder
-            .update(cx, |f, cx| f.spawn_search("hi".into(), cx))
+            .update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx))
             .await;
-        finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 7));
+        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
     }
 
     #[gpui::test]
@@ -472,20 +481,29 @@ mod tests {
         .await;
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) = cx.add_window(|cx| {
-            FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
+            Picker::new(
+                FileFinderDelegate::new(
+                    workspace.downgrade(),
+                    workspace.read(cx).project().clone(),
+                    None,
+                    cx,
+                ),
+                cx,
+            )
         });
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
         finder
-            .update(cx, |f, cx| f.spawn_search("thf".into(), cx))
+            .update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx))
             .await;
         cx.read(|cx| {
             let finder = finder.read(cx);
-            assert_eq!(finder.matches.len(), 1);
+            let delegate = finder.delegate();
+            assert_eq!(delegate.matches.len(), 1);
 
             let (file_name, file_name_positions, full_path, full_path_positions) =
-                finder.labels_for_match(&finder.matches[0]);
+                delegate.labels_for_match(&delegate.matches[0]);
             assert_eq!(file_name, "the-file");
             assert_eq!(file_name_positions, &[0, 1, 4]);
             assert_eq!(full_path, "the-file");
@@ -495,9 +513,9 @@ mod tests {
         // Since the worktree root is a file, searching for its name followed by a slash does
         // not match anything.
         finder
-            .update(cx, |f, cx| f.spawn_search("thf/".into(), cx))
+            .update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx))
             .await;
-        finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0));
+        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
     }
 
     #[gpui::test]
@@ -526,22 +544,31 @@ mod tests {
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let (_, finder) = cx.add_window(|cx| {
-            FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
+            Picker::new(
+                FileFinderDelegate::new(
+                    workspace.downgrade(),
+                    workspace.read(cx).project().clone(),
+                    None,
+                    cx,
+                ),
+                cx,
+            )
         });
 
         // Run a search that matches two files with the same relative path.
         finder
-            .update(cx, |f, cx| f.spawn_search("a.t".into(), cx))
+            .update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx))
             .await;
 
         // Can switch between different matches with the same relative path.
-        finder.update(cx, |f, cx| {
-            assert_eq!(f.matches.len(), 2);
-            assert_eq!(f.selected_index(), 0);
-            f.set_selected_index(1, cx);
-            assert_eq!(f.selected_index(), 1);
-            f.set_selected_index(0, cx);
-            assert_eq!(f.selected_index(), 0);
+        finder.update(cx, |finder, cx| {
+            let delegate = finder.delegate_mut();
+            assert_eq!(delegate.matches.len(), 2);
+            assert_eq!(delegate.selected_index(), 0);
+            delegate.set_selected_index(1, cx);
+            assert_eq!(delegate.selected_index(), 1);
+            delegate.set_selected_index(0, cx);
+            assert_eq!(delegate.selected_index(), 0);
         });
     }
 
@@ -573,16 +600,27 @@ mod tests {
         // so that one should be sorted earlier
         let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
         let (_, finder) = cx.add_window(|cx| {
-            FileFinderDelegate::new(workspace.read(cx).project().clone(), b_path, cx)
+            Picker::new(
+                FileFinderDelegate::new(
+                    workspace.downgrade(),
+                    workspace.read(cx).project().clone(),
+                    b_path,
+                    cx,
+                ),
+                cx,
+            )
         });
 
         finder
-            .update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
+            .update(cx, |f, cx| {
+                f.delegate_mut().spawn_search("a.txt".into(), cx)
+            })
             .await;
 
         finder.read_with(cx, |f, _| {
-            assert_eq!(f.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
-            assert_eq!(f.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
+            let delegate = f.delegate();
+            assert_eq!(delegate.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
+            assert_eq!(delegate.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
         });
     }
 
@@ -606,14 +644,22 @@ mod tests {
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) = cx.add_window(|cx| {
-            FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
+            Picker::new(
+                FileFinderDelegate::new(
+                    workspace.downgrade(),
+                    workspace.read(cx).project().clone(),
+                    None,
+                    cx,
+                ),
+                cx,
+            )
         });
         finder
-            .update(cx, |f, cx| f.spawn_search("dir".into(), cx))
+            .update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx))
             .await;
         cx.read(|cx| {
             let finder = finder.read(cx);
-            assert_eq!(finder.matches.len(), 0);
+            assert_eq!(finder.delegate().matches.len(), 0);
         });
     }
 }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -8,7 +8,7 @@ use gpui::{
 use menu::{Cancel, Confirm};
 use settings::Settings;
 use text::{Bias, Point};
-use workspace::Workspace;
+use workspace::{Modal, Workspace};
 
 actions!(go_to_line, [Toggle]);
 
@@ -65,11 +65,7 @@ impl GoToLine {
             .active_item(cx)
             .and_then(|active_item| active_item.downcast::<Editor>())
         {
-            workspace.toggle_modal(cx, |_, cx| {
-                let view = cx.add_view(|cx| GoToLine::new(editor, cx));
-                cx.subscribe(&view, Self::on_event).detach();
-                view
-            });
+            workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| GoToLine::new(editor, cx)));
         }
     }
 
@@ -91,17 +87,6 @@ impl GoToLine {
         cx.emit(Event::Dismissed);
     }
 
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<Self>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => workspace.dismiss_modal(cx),
-        }
-    }
-
     fn on_line_editor_event(
         &mut self,
         _: ViewHandle<Editor>,
@@ -194,3 +179,9 @@ impl View for GoToLine {
         cx.focus(&self.line_editor);
     }
 }
+
+impl Modal for GoToLine {
+    fn dismiss_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Dismissed)
+    }
+}

crates/language_selector/src/language_selector.rs 🔗

@@ -4,12 +4,9 @@ pub use active_buffer_language::ActiveBufferLanguage;
 use anyhow::anyhow;
 use editor::Editor;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
-use gpui::{
-    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, View,
-    ViewContext, ViewHandle,
-};
+use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContext};
 use language::{Buffer, LanguageRegistry};
-use picker::{Picker, PickerDelegate};
+use picker::{Picker, PickerDelegate, PickerEvent};
 use project::Project;
 use settings::Settings;
 use std::sync::Arc;
@@ -19,39 +16,49 @@ use workspace::{AppState, Workspace};
 actions!(language_selector, [Toggle]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
-    Picker::<LanguageSelector>::init(cx);
+    Picker::<LanguageSelectorDelegate>::init(cx);
     cx.add_action({
         let language_registry = app_state.languages.clone();
-        move |workspace, _: &Toggle, cx| {
-            LanguageSelector::toggle(workspace, language_registry.clone(), cx)
-        }
+        move |workspace, _: &Toggle, cx| toggle(workspace, language_registry.clone(), cx)
     });
 }
 
-pub enum Event {
-    Dismissed,
+fn toggle(
+    workspace: &mut Workspace,
+    registry: Arc<LanguageRegistry>,
+    cx: &mut ViewContext<Workspace>,
+) -> Option<()> {
+    let (_, buffer, _) = workspace
+        .active_item(cx)?
+        .act_as::<Editor>(cx)?
+        .read(cx)
+        .active_excerpt(cx)?;
+    workspace.toggle_modal(cx, |workspace, cx| {
+        cx.add_view(|cx| {
+            Picker::new(
+                LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry),
+                cx,
+            )
+        })
+    });
+    Some(())
 }
 
-pub struct LanguageSelector {
+pub struct LanguageSelectorDelegate {
     buffer: ModelHandle<Buffer>,
     project: ModelHandle<Project>,
     language_registry: Arc<LanguageRegistry>,
     candidates: Vec<StringMatchCandidate>,
     matches: Vec<StringMatch>,
-    picker: ViewHandle<Picker<Self>>,
     selected_index: usize,
 }
 
-impl LanguageSelector {
+impl LanguageSelectorDelegate {
     fn new(
         buffer: ModelHandle<Buffer>,
         project: ModelHandle<Project>,
         language_registry: Arc<LanguageRegistry>,
-        cx: &mut ViewContext<Self>,
     ) -> Self {
-        let handle = cx.weak_handle();
-        let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx));
-
         let candidates = language_registry
             .language_names()
             .into_iter()
@@ -75,70 +82,21 @@ impl LanguageSelector {
             language_registry,
             candidates,
             matches,
-            picker,
             selected_index: 0,
         }
     }
-
-    fn toggle(
-        workspace: &mut Workspace,
-        registry: Arc<LanguageRegistry>,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        if let Some((_, buffer, _)) = workspace
-            .active_item(cx)
-            .and_then(|active_item| active_item.act_as::<Editor>(cx))
-            .and_then(|editor| editor.read(cx).active_excerpt(cx))
-        {
-            workspace.toggle_modal(cx, |workspace, cx| {
-                let project = workspace.project().clone();
-                let this = cx.add_view(|cx| Self::new(buffer, project, registry, cx));
-                cx.subscribe(&this, Self::on_event).detach();
-                this
-            });
-        }
-    }
-
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<LanguageSelector>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => {
-                workspace.dismiss_modal(cx);
-            }
-        }
-    }
-}
-
-impl Entity for LanguageSelector {
-    type Event = Event;
 }
 
-impl View for LanguageSelector {
-    fn ui_name() -> &'static str {
-        "LanguageSelector"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
-        ChildView::new(&self.picker, cx).boxed()
+impl PickerDelegate for LanguageSelectorDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select a language...".into()
     }
 
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.picker);
-        }
-    }
-}
-
-impl PickerDelegate for LanguageSelector {
     fn match_count(&self) -> usize {
         self.matches.len()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
             let language_name = &self.candidates[mat.candidate_id].string;
             let language = self.language_registry.language_for_name(language_name);
@@ -160,22 +118,24 @@ impl PickerDelegate for LanguageSelector {
             .detach_and_log_err(cx);
         }
 
-        cx.emit(Event::Dismissed);
+        cx.emit(PickerEvent::Dismiss);
     }
 
-    fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Dismissed);
-    }
+    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
 
     fn selected_index(&self) -> usize {
         self.selected_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
         self.selected_index = ix;
     }
 
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> gpui::Task<()> {
         let background = cx.background().clone();
         let candidates = self.candidates.clone();
         cx.spawn_weak(|this, mut cx| async move {
@@ -204,10 +164,11 @@ impl PickerDelegate for LanguageSelector {
 
             if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
-                    this.matches = matches;
-                    this.selected_index = this
+                    let delegate = this.delegate_mut();
+                    delegate.matches = matches;
+                    delegate.selected_index = delegate
                         .selected_index
-                        .min(this.matches.len().saturating_sub(1));
+                        .min(delegate.matches.len().saturating_sub(1));
                     cx.notify();
                 })
                 .log_err();

crates/outline/src/outline.rs 🔗

@@ -4,25 +4,51 @@ use editor::{
 };
 use fuzzy::StringMatch;
 use gpui::{
-    actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Entity,
-    MouseState, Task, View, ViewContext, ViewHandle, WindowContext,
+    actions, elements::*, geometry::vector::Vector2F, AppContext, MouseState, Task, ViewContext,
+    ViewHandle, WindowContext,
 };
 use language::Outline;
 use ordered_float::OrderedFloat;
-use picker::{Picker, PickerDelegate};
+use picker::{Picker, PickerDelegate, PickerEvent};
 use settings::Settings;
-use std::cmp::{self, Reverse};
+use std::{
+    cmp::{self, Reverse},
+    sync::Arc,
+};
 use workspace::Workspace;
 
 actions!(outline, [Toggle]);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(OutlineView::toggle);
-    Picker::<OutlineView>::init(cx);
+    cx.add_action(toggle);
+    OutlineView::init(cx);
+}
+
+fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+    if let Some(editor) = workspace
+        .active_item(cx)
+        .and_then(|item| item.downcast::<Editor>())
+    {
+        let outline = editor
+            .read(cx)
+            .buffer()
+            .read(cx)
+            .snapshot(cx)
+            .outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
+        if let Some(outline) = outline {
+            workspace.toggle_modal(cx, |_, cx| {
+                cx.add_view(|cx| {
+                    OutlineView::new(OutlineViewDelegate::new(outline, editor, cx), cx)
+                        .with_max_size(800., 1200.)
+                })
+            });
+        }
+    }
 }
 
-struct OutlineView {
-    picker: ViewHandle<Picker<Self>>,
+type OutlineView = Picker<OutlineViewDelegate>;
+
+struct OutlineViewDelegate {
     active_editor: ViewHandle<Editor>,
     outline: Outline<Anchor>,
     selected_match_index: usize,
@@ -31,47 +57,13 @@ struct OutlineView {
     last_query: String,
 }
 
-pub enum Event {
-    Dismissed,
-}
-
-impl Entity for OutlineView {
-    type Event = Event;
-
-    fn release(&mut self, cx: &mut AppContext) {
-        cx.update_window(self.active_editor.window_id(), |cx| {
-            self.restore_active_editor(cx);
-        });
-    }
-}
-
-impl View for OutlineView {
-    fn ui_name() -> &'static str {
-        "OutlineView"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
-        ChildView::new(&self.picker, cx).boxed()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.picker);
-        }
-    }
-}
-
-impl OutlineView {
+impl OutlineViewDelegate {
     fn new(
         outline: Outline<Anchor>,
         editor: ViewHandle<Editor>,
-        cx: &mut ViewContext<Self>,
+        cx: &mut ViewContext<OutlineView>,
     ) -> Self {
-        let handle = cx.weak_handle();
         Self {
-            picker: cx.add_view(|cx| {
-                Picker::new("Search buffer symbols...", handle, cx).with_max_size(800., 1200.)
-            }),
             last_query: Default::default(),
             matches: Default::default(),
             selected_match_index: 0,
@@ -81,27 +73,6 @@ impl OutlineView {
         }
     }
 
-    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        if let Some(editor) = workspace
-            .active_item(cx)
-            .and_then(|item| item.downcast::<Editor>())
-        {
-            let outline = editor
-                .read(cx)
-                .buffer()
-                .read(cx)
-                .snapshot(cx)
-                .outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
-            if let Some(outline) = outline {
-                workspace.toggle_modal(cx, |_, cx| {
-                    let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
-                    cx.subscribe(&view, Self::on_event).detach();
-                    view
-                });
-            }
-        }
-    }
-
     fn restore_active_editor(&mut self, cx: &mut WindowContext) {
         self.active_editor.update(cx, |editor, cx| {
             editor.highlight_rows(None);
@@ -111,7 +82,7 @@ impl OutlineView {
         })
     }
 
-    fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext<OutlineView>) {
         self.selected_match_index = ix;
         if navigate && !self.matches.is_empty() {
             let selected_match = &self.matches[self.selected_match_index];
@@ -127,22 +98,14 @@ impl OutlineView {
                 active_editor.request_autoscroll(Autoscroll::center(), cx);
             });
         }
-        cx.notify();
     }
+}
 
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<Self>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => workspace.dismiss_modal(cx),
-        }
+impl PickerDelegate for OutlineViewDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search buffer symbols...".into()
     }
-}
 
-impl PickerDelegate for OutlineView {
     fn match_count(&self) -> usize {
         self.matches.len()
     }
@@ -151,7 +114,7 @@ impl PickerDelegate for OutlineView {
         self.selected_match_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<OutlineView>) {
         self.set_selected_index(ix, true, cx);
     }
 
@@ -159,7 +122,7 @@ impl PickerDelegate for OutlineView {
         true
     }
 
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<OutlineView>) -> Task<()> {
         let selected_index;
         if query.is_empty() {
             self.restore_active_editor(cx);
@@ -215,7 +178,7 @@ impl PickerDelegate for OutlineView {
         Task::ready(())
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<OutlineView>) {
         self.prev_scroll_position.take();
         self.active_editor.update(cx, |active_editor, cx| {
             if let Some(rows) = active_editor.highlighted_rows() {
@@ -226,12 +189,11 @@ impl PickerDelegate for OutlineView {
                 });
             }
         });
-        cx.emit(Event::Dismissed);
+        cx.emit(PickerEvent::Dismiss);
     }
 
-    fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
+    fn dismissed(&mut self, cx: &mut ViewContext<OutlineView>) {
         self.restore_active_editor(cx);
-        cx.emit(Event::Dismissed);
     }
 
     fn render_match(

crates/picker/src/picker.rs 🔗

@@ -24,6 +24,7 @@ pub struct Picker<D: PickerDelegate> {
     max_size: Vector2F,
     theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
     confirmed: bool,
+    pending_update_matches: Task<Option<()>>,
 }
 
 pub trait PickerDelegate: Sized + 'static {
@@ -184,6 +185,7 @@ impl<D: PickerDelegate> Picker<D> {
             max_size: vec2f(540., 420.),
             theme,
             confirmed: false,
+            pending_update_matches: Task::ready(None),
         };
         // TODO! How can the delegate notify the picker to update?
         // cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
@@ -238,22 +240,24 @@ impl<D: PickerDelegate> Picker<D> {
 
     pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
         let update = self.delegate.update_matches(query, cx);
-        cx.spawn_weak(|this, mut cx| async move {
+        self.matches_updated(cx);
+        self.pending_update_matches = cx.spawn_weak(|this, mut cx| async move {
             update.await;
             this.upgrade(&cx)?
-                .update(&mut cx, |this, cx| {
-                    let index = this.delegate.selected_index();
-                    let target = if this.delegate.center_selection_after_match_updates() {
-                        ScrollTarget::Center(index)
-                    } else {
-                        ScrollTarget::Show(index)
-                    };
-                    this.list_state.scroll_to(target);
-                    cx.notify();
-                })
+                .update(&mut cx, |this, cx| this.matches_updated(cx))
                 .log_err()
-        })
-        .detach()
+        });
+    }
+
+    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
+        let index = self.delegate.selected_index();
+        let target = if self.delegate.center_selection_after_match_updates() {
+            ScrollTarget::Center(index)
+        } else {
+            ScrollTarget::Show(index)
+        };
+        self.list_state.scroll_to(target);
+        cx.notify();
     }
 
     pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
@@ -306,7 +310,7 @@ impl<D: PickerDelegate> Picker<D> {
         cx.notify();
     }
 
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+    pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         self.confirmed = true;
         self.delegate.confirm(cx);
     }

crates/project_symbols/src/project_symbols.rs 🔗

@@ -1,90 +1,63 @@
+use anyhow::anyhow;
 use editor::{
     combine_syntax_and_fuzzy_match_highlights, scroll::autoscroll::Autoscroll,
     styled_runs_for_code_label, Bias, Editor,
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, Task, View,
-    ViewContext, ViewHandle,
+    actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
 };
 use ordered_float::OrderedFloat;
-use picker::{Picker, PickerDelegate};
+use picker::{Picker, PickerDelegate, PickerEvent};
 use project::{Project, Symbol};
 use settings::Settings;
-use std::{borrow::Cow, cmp::Reverse};
+use std::{borrow::Cow, cmp::Reverse, sync::Arc};
 use util::ResultExt;
 use workspace::Workspace;
 
 actions!(project_symbols, [Toggle]);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(ProjectSymbolsView::toggle);
-    Picker::<ProjectSymbolsView>::init(cx);
+    cx.add_action(toggle);
+    ProjectSymbols::init(cx);
 }
 
-pub struct ProjectSymbolsView {
-    picker: ViewHandle<Picker<Self>>,
+fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+    workspace.toggle_modal(cx, |workspace, cx| {
+        let project = workspace.project().clone();
+        let workspace = cx.weak_handle();
+        cx.add_view(|cx| ProjectSymbols::new(ProjectSymbolsDelegate::new(workspace, project), cx))
+    });
+}
+
+pub type ProjectSymbols = Picker<ProjectSymbolsDelegate>;
+
+pub struct ProjectSymbolsDelegate {
+    workspace: WeakViewHandle<Workspace>,
     project: ModelHandle<Project>,
     selected_match_index: usize,
     symbols: Vec<Symbol>,
     visible_match_candidates: Vec<StringMatchCandidate>,
     external_match_candidates: Vec<StringMatchCandidate>,
     show_worktree_root_name: bool,
-    pending_update: Task<()>,
     matches: Vec<StringMatch>,
 }
 
-pub enum Event {
-    Dismissed,
-    Selected(Symbol),
-}
-
-impl Entity for ProjectSymbolsView {
-    type Event = Event;
-}
-
-impl View for ProjectSymbolsView {
-    fn ui_name() -> &'static str {
-        "ProjectSymbolsView"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
-        ChildView::new(&self.picker, cx).boxed()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.picker);
-        }
-    }
-}
-
-impl ProjectSymbolsView {
-    fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
-        let handle = cx.weak_handle();
+impl ProjectSymbolsDelegate {
+    fn new(workspace: WeakViewHandle<Workspace>, project: ModelHandle<Project>) -> Self {
         Self {
+            workspace,
             project,
-            picker: cx.add_view(|cx| Picker::new("Search project symbols...", handle, cx)),
             selected_match_index: 0,
             symbols: Default::default(),
             visible_match_candidates: Default::default(),
             external_match_candidates: Default::default(),
             matches: Default::default(),
             show_worktree_root_name: false,
-            pending_update: Task::ready(()),
         }
     }
 
-    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        workspace.toggle_modal(cx, |workspace, cx| {
-            let project = workspace.project().clone();
-            let symbols = cx.add_view(|cx| Self::new(project, cx));
-            cx.subscribe(&symbols, Self::on_event).detach();
-            symbols
-        });
-    }
-
-    fn filter(&mut self, query: &str, cx: &mut ViewContext<Self>) {
+    fn filter(&mut self, query: &str, cx: &mut ViewContext<ProjectSymbols>) {
         const MAX_MATCHES: usize = 100;
         let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
             &self.visible_match_candidates,
@@ -125,60 +98,50 @@ impl ProjectSymbolsView {
 
         self.matches = matches;
         self.set_selected_index(0, cx);
-        cx.notify();
     }
+}
 
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<Self>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => workspace.dismiss_modal(cx),
-            Event::Selected(symbol) => {
-                let buffer = workspace
-                    .project()
-                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
-
-                let symbol = symbol.clone();
-                cx.spawn(|workspace, mut cx| async move {
-                    let buffer = buffer.await?;
-                    workspace.update(&mut cx, |workspace, cx| {
-                        let position = buffer
-                            .read(cx)
-                            .clip_point_utf16(symbol.range.start, Bias::Left);
-
-                        let editor = workspace.open_project_item::<Editor>(buffer, cx);
-                        editor.update(cx, |editor, cx| {
-                            editor.change_selections(Some(Autoscroll::center()), cx, |s| {
-                                s.select_ranges([position..position])
-                            });
-                        });
-                    })?;
-                    Ok::<_, anyhow::Error>(())
-                })
-                .detach_and_log_err(cx);
-                workspace.dismiss_modal(cx);
-            }
-        }
+impl PickerDelegate for ProjectSymbolsDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search project symbols...".into()
     }
-}
 
-impl PickerDelegate for ProjectSymbolsView {
-    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<ProjectSymbols>) {
         if let Some(symbol) = self
             .matches
             .get(self.selected_match_index)
             .map(|mat| self.symbols[mat.candidate_id].clone())
         {
-            cx.emit(Event::Selected(symbol));
+            let buffer = self.project.update(cx, |project, cx| {
+                project.open_buffer_for_symbol(&symbol, cx)
+            });
+            let symbol = symbol.clone();
+            let workspace = self.workspace.clone();
+            cx.spawn_weak(|_, mut cx| async move {
+                let buffer = buffer.await?;
+                let workspace = workspace
+                    .upgrade(&cx)
+                    .ok_or_else(|| anyhow!("workspace was dropped"))?;
+                workspace.update(&mut cx, |workspace, cx| {
+                    let position = buffer
+                        .read(cx)
+                        .clip_point_utf16(symbol.range.start, Bias::Left);
+
+                    let editor = workspace.open_project_item::<Editor>(buffer, cx);
+                    editor.update(cx, |editor, cx| {
+                        editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+                            s.select_ranges([position..position])
+                        });
+                    });
+                })?;
+                Ok::<_, anyhow::Error>(())
+            })
+            .detach_and_log_err(cx);
+            cx.emit(PickerEvent::Dismiss);
         }
     }
 
-    fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Dismissed);
-    }
+    fn dismissed(&mut self, _cx: &mut ViewContext<ProjectSymbols>) {}
 
     fn match_count(&self) -> usize {
         self.matches.len()
@@ -188,23 +151,23 @@ impl PickerDelegate for ProjectSymbolsView {
         self.selected_match_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<ProjectSymbols>) {
         self.selected_match_index = ix;
-        cx.notify();
     }
 
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<ProjectSymbols>) -> Task<()> {
         self.filter(&query, cx);
         self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
         let symbols = self
             .project
             .update(cx, |project, cx| project.symbols(&query, cx));
-        self.pending_update = cx.spawn_weak(|this, mut cx| async move {
+        cx.spawn_weak(|this, mut cx| async move {
             let symbols = symbols.await.log_err();
             if let Some(this) = this.upgrade(&cx) {
                 if let Some(symbols) = symbols {
                     this.update(&mut cx, |this, cx| {
-                        let project = this.project.read(cx);
+                        let delegate = this.delegate_mut();
+                        let project = delegate.project.read(cx);
                         let (visible_match_candidates, external_match_candidates) = symbols
                             .iter()
                             .enumerate()
@@ -221,16 +184,15 @@ impl PickerDelegate for ProjectSymbolsView {
                                     .map_or(false, |e| !e.is_ignored)
                             });
 
-                        this.visible_match_candidates = visible_match_candidates;
-                        this.external_match_candidates = external_match_candidates;
-                        this.symbols = symbols;
-                        this.filter(&query, cx);
+                        delegate.visible_match_candidates = visible_match_candidates;
+                        delegate.external_match_candidates = external_match_candidates;
+                        delegate.symbols = symbols;
+                        delegate.filter(&query, cx);
                     })
                     .log_err();
                 }
             }
-        });
-        Task::ready(())
+        })
     }
 
     fn render_match(
@@ -364,46 +326,53 @@ mod tests {
             },
         );
 
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+
         // Create the project symbols view.
-        let (_, symbols_view) = cx.add_window(|cx| ProjectSymbolsView::new(project.clone(), cx));
-        let picker = symbols_view.read_with(cx, |symbols_view, _| symbols_view.picker.clone());
+        let symbols = cx.add_view(&workspace, |cx| {
+            ProjectSymbols::new(
+                ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
+                cx,
+            )
+        });
 
         // Spawn multiples updates before the first update completes,
         // such that in the end, there are no matches. Testing for regression:
         // https://github.com/zed-industries/zed/issues/861
-        picker.update(cx, |p, cx| {
+        symbols.update(cx, |p, cx| {
             p.update_matches("o".to_string(), cx);
             p.update_matches("on".to_string(), cx);
             p.update_matches("onex".to_string(), cx);
         });
 
         cx.foreground().run_until_parked();
-        symbols_view.read_with(cx, |symbols_view, _| {
-            assert_eq!(symbols_view.matches.len(), 0);
+        symbols.read_with(cx, |symbols, _| {
+            assert_eq!(symbols.delegate().matches.len(), 0);
         });
 
         // Spawn more updates such that in the end, there are matches.
-        picker.update(cx, |p, cx| {
+        symbols.update(cx, |p, cx| {
             p.update_matches("one".to_string(), cx);
             p.update_matches("on".to_string(), cx);
         });
 
         cx.foreground().run_until_parked();
-        symbols_view.read_with(cx, |symbols_view, _| {
-            assert_eq!(symbols_view.matches.len(), 2);
-            assert_eq!(symbols_view.matches[0].string, "ton");
-            assert_eq!(symbols_view.matches[1].string, "one");
+        symbols.read_with(cx, |symbols, _| {
+            let delegate = symbols.delegate();
+            assert_eq!(delegate.matches.len(), 2);
+            assert_eq!(delegate.matches[0].string, "ton");
+            assert_eq!(delegate.matches[1].string, "one");
         });
 
         // Spawn more updates such that in the end, there are again no matches.
-        picker.update(cx, |p, cx| {
+        symbols.update(cx, |p, cx| {
             p.update_matches("o".to_string(), cx);
             p.update_matches("".to_string(), cx);
         });
 
         cx.foreground().run_until_parked();
-        symbols_view.read_with(cx, |symbols_view, _| {
-            assert_eq!(symbols_view.matches.len(), 0);
+        symbols.read_with(cx, |symbols, _| {
+            assert_eq!(symbols.delegate().matches.len(), 0);
         });
     }
 

crates/recent_projects/src/recent_projects.rs 🔗

@@ -3,14 +3,15 @@ mod highlighted_workspace_location;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
-    elements::{ChildView, Flex, ParentElement},
-    AnyViewHandle, AppContext, Drawable, Element, Entity, Task, View, ViewContext, ViewHandle,
+    anyhow::Result,
+    elements::{Flex, ParentElement},
+    AppContext, Drawable, Element, Task, ViewContext,
 };
 use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
-use picker::{Picker, PickerDelegate};
+use picker::{Picker, PickerDelegate, PickerEvent};
 use settings::Settings;
-use util::ResultExt;
+use std::sync::Arc;
 use workspace::{
     notifications::simple_message_notification::MessageNotification, OpenPaths, Workspace,
     WorkspaceLocation, WORKSPACE_DB,
@@ -19,103 +20,70 @@ use workspace::{
 actions!(projects, [OpenRecent]);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(RecentProjectsView::toggle);
-    Picker::<RecentProjectsView>::init(cx);
+    cx.add_async_action(toggle);
+    RecentProjects::init(cx);
 }
 
-struct RecentProjectsView {
-    picker: ViewHandle<Picker<Self>>,
+fn toggle(
+    _: &mut Workspace,
+    _: &OpenRecent,
+    cx: &mut ViewContext<Workspace>,
+) -> Option<Task<Result<()>>> {
+    Some(cx.spawn(|workspace, mut cx| async move {
+        let workspace_locations: Vec<_> = cx
+            .background()
+            .spawn(async {
+                WORKSPACE_DB
+                    .recent_workspaces_on_disk()
+                    .await
+                    .unwrap_or_default()
+                    .into_iter()
+                    .map(|(_, location)| location)
+                    .collect()
+            })
+            .await;
+
+        workspace.update(&mut cx, |workspace, cx| {
+            if !workspace_locations.is_empty() {
+                workspace.toggle_modal(cx, |_, cx| {
+                    cx.add_view(|cx| {
+                        RecentProjects::new(RecentProjectsDelegate::new(workspace_locations), cx)
+                            .with_max_size(800., 1200.)
+                    })
+                });
+            } else {
+                workspace.show_notification(0, cx, |cx| {
+                    cx.add_view(|_| MessageNotification::new_message("No recent projects to open."))
+                })
+            }
+        })?;
+        Ok(())
+    }))
+}
+
+type RecentProjects = Picker<RecentProjectsDelegate>;
+
+struct RecentProjectsDelegate {
     workspace_locations: Vec<WorkspaceLocation>,
     selected_match_index: usize,
     matches: Vec<StringMatch>,
 }
 
-impl RecentProjectsView {
-    fn new(workspace_locations: Vec<WorkspaceLocation>, cx: &mut ViewContext<Self>) -> Self {
-        let handle = cx.weak_handle();
+impl RecentProjectsDelegate {
+    fn new(workspace_locations: Vec<WorkspaceLocation>) -> Self {
         Self {
-            picker: cx.add_view(|cx| {
-                Picker::new("Recent Projects...", handle, cx).with_max_size(800., 1200.)
-            }),
             workspace_locations,
             selected_match_index: 0,
             matches: Default::default(),
         }
     }
-
-    fn toggle(_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>) {
-        cx.spawn(|workspace, mut cx| async move {
-            let workspace_locations: Vec<_> = cx
-                .background()
-                .spawn(async {
-                    WORKSPACE_DB
-                        .recent_workspaces_on_disk()
-                        .await
-                        .unwrap_or_default()
-                        .into_iter()
-                        .map(|(_, location)| location)
-                        .collect()
-                })
-                .await;
-
-            workspace
-                .update(&mut cx, |workspace, cx| {
-                    if !workspace_locations.is_empty() {
-                        workspace.toggle_modal(cx, |_, cx| {
-                            let view = cx.add_view(|cx| Self::new(workspace_locations, cx));
-                            cx.subscribe(&view, Self::on_event).detach();
-                            view
-                        });
-                    } else {
-                        workspace.show_notification(0, cx, |cx| {
-                            cx.add_view(|_| {
-                                MessageNotification::new_message("No recent projects to open.")
-                            })
-                        })
-                    }
-                })
-                .log_err();
-        })
-        .detach();
-    }
-
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<Self>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => workspace.dismiss_modal(cx),
-        }
-    }
-}
-
-pub enum Event {
-    Dismissed,
 }
 
-impl Entity for RecentProjectsView {
-    type Event = Event;
-}
-
-impl View for RecentProjectsView {
-    fn ui_name() -> &'static str {
-        "RecentProjectsView"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
-        ChildView::new(&self.picker, cx).boxed()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.picker);
-        }
+impl PickerDelegate for RecentProjectsDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Recent Projects...".into()
     }
-}
 
-impl PickerDelegate for RecentProjectsView {
     fn match_count(&self) -> usize {
         self.matches.len()
     }
@@ -124,11 +92,15 @@ impl PickerDelegate for RecentProjectsView {
         self.selected_match_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
         self.selected_match_index = ix;
     }
 
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<RecentProjects>,
+    ) -> gpui::Task<()> {
         let query = query.trim_start();
         let smart_case = query.chars().any(|c| c.is_uppercase());
         let candidates = self
@@ -166,19 +138,17 @@ impl PickerDelegate for RecentProjectsView {
         Task::ready(())
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
         if let Some(selected_match) = &self.matches.get(self.selected_index()) {
             let workspace_location = &self.workspace_locations[selected_match.candidate_id];
             cx.dispatch_global_action(OpenPaths {
                 paths: workspace_location.paths().as_ref().clone(),
             });
-            cx.emit(Event::Dismissed);
+            cx.emit(PickerEvent::Dismiss);
         }
     }
 
-    fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Dismissed);
-    }
+    fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
 
     fn render_match(
         &self,

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,9 +1,6 @@
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
-use gpui::{
-    actions, elements::*, AnyViewHandle, AppContext, Drawable, Element, Entity, MouseState, View,
-    ViewContext, ViewHandle,
-};
-use picker::{Picker, PickerDelegate};
+use gpui::{actions, elements::*, AppContext, Drawable, Element, MouseState, ViewContext};
+use picker::{Picker, PickerDelegate, PickerEvent};
 use settings::{settings_file::SettingsFile, Settings};
 use staff_mode::StaffMode;
 use std::sync::Arc;
@@ -11,36 +8,50 @@ use theme::{Theme, ThemeMeta, ThemeRegistry};
 use util::ResultExt;
 use workspace::{AppState, Workspace};
 
-pub struct ThemeSelector {
-    registry: Arc<ThemeRegistry>,
-    theme_data: Vec<ThemeMeta>,
-    matches: Vec<StringMatch>,
-    original_theme: Arc<Theme>,
-    picker: ViewHandle<Picker<Self>>,
-    selection_completed: bool,
-    selected_index: usize,
-}
-
 actions!(theme_selector, [Toggle, Reload]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
-    Picker::<ThemeSelector>::init(cx);
     cx.add_action({
         let theme_registry = app_state.themes.clone();
-        move |workspace, _: &Toggle, cx| {
-            ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
-        }
+        move |workspace, _: &Toggle, cx| toggle(workspace, theme_registry.clone(), cx)
     });
+    ThemeSelector::init(cx);
 }
 
-pub enum Event {
-    Dismissed,
+fn toggle(workspace: &mut Workspace, themes: Arc<ThemeRegistry>, cx: &mut ViewContext<Workspace>) {
+    workspace.toggle_modal(cx, |_, cx| {
+        cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
+    });
 }
 
-impl ThemeSelector {
-    fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<Self>) -> Self {
-        let handle = cx.weak_handle();
-        let picker = cx.add_view(|cx| Picker::new("Select Theme...", handle, cx));
+#[cfg(debug_assertions)]
+pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
+    let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
+    themes.clear();
+    match themes.get(&current_theme_name) {
+        Ok(theme) => {
+            ThemeSelectorDelegate::set_theme(theme, cx);
+            log::info!("reloaded theme {}", current_theme_name);
+        }
+        Err(error) => {
+            log::error!("failed to load theme {}: {:?}", current_theme_name, error)
+        }
+    }
+}
+
+pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
+
+pub struct ThemeSelectorDelegate {
+    registry: Arc<ThemeRegistry>,
+    theme_data: Vec<ThemeMeta>,
+    matches: Vec<StringMatch>,
+    original_theme: Arc<Theme>,
+    selection_completed: bool,
+    selected_index: usize,
+}
+
+impl ThemeSelectorDelegate {
+    fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<ThemeSelector>) -> Self {
         let settings = cx.global::<Settings>();
 
         let original_theme = settings.theme.clone();
@@ -62,7 +73,6 @@ impl ThemeSelector {
             registry,
             theme_data: theme_names,
             matches,
-            picker,
             original_theme: original_theme.clone(),
             selected_index: 0,
             selection_completed: false,
@@ -71,34 +81,7 @@ impl ThemeSelector {
         this
     }
 
-    fn toggle(
-        workspace: &mut Workspace,
-        themes: Arc<ThemeRegistry>,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        workspace.toggle_modal(cx, |_, cx| {
-            let this = cx.add_view(|cx| Self::new(themes, cx));
-            cx.subscribe(&this, Self::on_event).detach();
-            this
-        });
-    }
-
-    #[cfg(debug_assertions)]
-    pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
-        let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
-        themes.clear();
-        match themes.get(&current_theme_name) {
-            Ok(theme) => {
-                Self::set_theme(theme, cx);
-                log::info!("reloaded theme {}", current_theme_name);
-            }
-            Err(error) => {
-                log::error!("failed to load theme {}: {:?}", current_theme_name, error)
-            }
-        }
-    }
-
-    fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
+    fn show_selected_theme(&mut self, cx: &mut ViewContext<ThemeSelector>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
             match self.registry.get(&mat.string) {
                 Ok(theme) => {
@@ -119,19 +102,6 @@ impl ThemeSelector {
             .unwrap_or(self.selected_index);
     }
 
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<ThemeSelector>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => {
-                workspace.dismiss_modal(cx);
-            }
-        }
-    }
-
     fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
         cx.update_global::<Settings, _, _>(|settings, cx| {
             settings.theme = theme;
@@ -140,12 +110,16 @@ impl ThemeSelector {
     }
 }
 
-impl PickerDelegate for ThemeSelector {
+impl PickerDelegate for ThemeSelectorDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select Theme...".into()
+    }
+
     fn match_count(&self) -> usize {
         self.matches.len()
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<ThemeSelector>) {
         self.selection_completed = true;
 
         let theme_name = cx.global::<Settings>().theme.meta.name.clone();
@@ -153,27 +127,30 @@ impl PickerDelegate for ThemeSelector {
             settings_content.theme = Some(theme_name);
         });
 
-        cx.emit(Event::Dismissed);
+        cx.emit(PickerEvent::Dismiss);
     }
 
-    fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
+    fn dismissed(&mut self, cx: &mut ViewContext<ThemeSelector>) {
         if !self.selection_completed {
             Self::set_theme(self.original_theme.clone(), cx);
             self.selection_completed = true;
         }
-        cx.emit(Event::Dismissed);
     }
 
     fn selected_index(&self) -> usize {
         self.selected_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<ThemeSelector>) {
         self.selected_index = ix;
         self.show_selected_theme(cx);
     }
 
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<ThemeSelector>,
+    ) -> gpui::Task<()> {
         let background = cx.background().clone();
         let candidates = self
             .theme_data
@@ -212,12 +189,12 @@ impl PickerDelegate for ThemeSelector {
 
             if let Some(this) = this.upgrade(&cx) {
                 this.update(&mut cx, |this, cx| {
-                    this.matches = matches;
-                    this.selected_index = this
+                    let delegate = this.delegate_mut();
+                    delegate.matches = matches;
+                    delegate.selected_index = delegate
                         .selected_index
-                        .min(this.matches.len().saturating_sub(1));
-                    this.show_selected_theme(cx);
-                    cx.notify();
+                        .min(delegate.matches.len().saturating_sub(1));
+                    delegate.show_selected_theme(cx);
                 })
                 .log_err();
             }
@@ -243,29 +220,3 @@ impl PickerDelegate for ThemeSelector {
             .boxed()
     }
 }
-
-impl Entity for ThemeSelector {
-    type Event = Event;
-
-    fn release(&mut self, cx: &mut AppContext) {
-        if !self.selection_completed {
-            Self::set_theme(self.original_theme.clone(), cx);
-        }
-    }
-}
-
-impl View for ThemeSelector {
-    fn ui_name() -> &'static str {
-        "ThemeSelector"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
-        ChildView::new(&self.picker, cx).boxed()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.picker);
-        }
-    }
-}

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -1,92 +1,59 @@
+use std::sync::Arc;
+
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
-    elements::{ChildView, Drawable as _, Label},
-    AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle,
+    elements::{Drawable as _, Label},
+    AppContext, Task, ViewContext,
 };
-use picker::{Picker, PickerDelegate};
+use picker::{Picker, PickerDelegate, PickerEvent};
 use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
 use util::ResultExt;
 use workspace::Workspace;
 
-pub struct BaseKeymapSelector {
-    matches: Vec<StringMatch>,
-    picker: ViewHandle<Picker<Self>>,
-    selected_index: usize,
-}
-
 actions!(welcome, [ToggleBaseKeymapSelector]);
 
 pub fn init(cx: &mut AppContext) {
-    Picker::<BaseKeymapSelector>::init(cx);
-    cx.add_action({
-        move |workspace, _: &ToggleBaseKeymapSelector, cx| BaseKeymapSelector::toggle(workspace, cx)
-    });
+    cx.add_action(toggle);
+    BaseKeymapSelector::init(cx);
 }
 
-pub enum Event {
-    Dismissed,
+fn toggle(
+    workspace: &mut Workspace,
+    _: &ToggleBaseKeymapSelector,
+    cx: &mut ViewContext<Workspace>,
+) {
+    workspace.toggle_modal(cx, |_, cx| {
+        cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(cx), cx))
+    });
 }
 
-impl BaseKeymapSelector {
-    fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
-        workspace.toggle_modal(cx, |_, cx| {
-            let this = cx.add_view(|cx| Self::new(cx));
-            cx.subscribe(&this, Self::on_event).detach();
-            this
-        });
-    }
+pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
+
+pub struct BaseKeymapSelectorDelegate {
+    matches: Vec<StringMatch>,
+    selected_index: usize,
+}
 
-    fn new(cx: &mut ViewContext<Self>) -> Self {
+impl BaseKeymapSelectorDelegate {
+    fn new(cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
         let base = cx.global::<Settings>().base_keymap;
         let selected_index = BaseKeymap::OPTIONS
             .iter()
             .position(|(_, value)| *value == base)
             .unwrap_or(0);
-
-        let this = cx.weak_handle();
         Self {
-            picker: cx.add_view(|cx| Picker::new("Select a base keymap", this, cx)),
             matches: Vec::new(),
             selected_index,
         }
     }
-
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<BaseKeymapSelector>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => {
-                workspace.dismiss_modal(cx);
-            }
-        }
-    }
 }
 
-impl Entity for BaseKeymapSelector {
-    type Event = Event;
-}
-
-impl View for BaseKeymapSelector {
-    fn ui_name() -> &'static str {
-        "BaseKeymapSelector"
+impl PickerDelegate for BaseKeymapSelectorDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select a base keymap...".into()
     }
 
-    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::Element<Self> {
-        ChildView::new(&self.picker, cx).boxed()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.picker);
-        }
-    }
-}
-
-impl PickerDelegate for BaseKeymapSelector {
     fn match_count(&self) -> usize {
         self.matches.len()
     }
@@ -95,11 +62,15 @@ impl PickerDelegate for BaseKeymapSelector {
         self.selected_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<BaseKeymapSelector>) {
         self.selected_index = ix;
     }
 
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> Task<()> {
         let background = cx.background().clone();
         let candidates = BaseKeymap::names()
             .enumerate()
@@ -135,29 +106,27 @@ impl PickerDelegate for BaseKeymapSelector {
             };
 
             if let Some(this) = this.upgrade(&cx) {
-                this.update(&mut cx, |this, cx| {
-                    this.matches = matches;
-                    this.selected_index = this
+                this.update(&mut cx, |this, _| {
+                    let delegate = this.delegate_mut();
+                    delegate.matches = matches;
+                    delegate.selected_index = delegate
                         .selected_index
-                        .min(this.matches.len().saturating_sub(1));
-                    cx.notify();
+                        .min(delegate.matches.len().saturating_sub(1));
                 })
                 .log_err();
             }
         })
     }
 
-    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
             SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
         }
-        cx.emit(Event::Dismissed);
+        cx.emit(PickerEvent::Dismiss);
     }
 
-    fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Dismissed)
-    }
+    fn dismissed(&mut self, _cx: &mut ViewContext<BaseKeymapSelector>) {}
 
     fn render_match(
         &self,

crates/zed/src/main.rs 🔗

@@ -520,7 +520,7 @@ async fn watch_themes(
             .await
             .log_err()?;
         if output.status.success() {
-            cx.update(|cx| theme_selector::ThemeSelector::reload(themes.clone(), cx))
+            cx.update(|cx| theme_selector::reload(themes.clone(), cx))
         } else {
             eprintln!(
                 "build script failed {}",