Remove 2 suffix for picker, feedback

Max Brunsfeld and Mikayla created

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

Change summary

Cargo.lock                                     |  73 --
Cargo.toml                                     |   1 
crates/collab_ui/Cargo.toml                    |   4 
crates/command_palette/Cargo.toml              |   2 
crates/command_palette2/Cargo.toml             |  36 +
crates/feedback/Cargo.toml                     |  34 
crates/feedback/src/deploy_feedback_button.rs  |  98 +--
crates/feedback/src/feedback.rs                |  91 +-
crates/feedback/src/feedback_editor.rs         | 442 -----------------
crates/feedback/src/feedback_info_text.rs      |  94 ---
crates/feedback/src/feedback_modal.rs          |   0 
crates/feedback/src/submit_feedback_button.rs  | 108 ----
crates/feedback/src/system_specs.rs            |  27 
crates/feedback2/Cargo.toml                    |  47 -
crates/feedback2/src/deploy_feedback_button.rs |  49 -
crates/feedback2/src/feedback2.rs              |  61 --
crates/feedback2/src/system_specs.rs           |  68 --
crates/file_finder/Cargo.toml                  |   2 
crates/language_selector/Cargo.toml            |   2 
crates/outline2/Cargo.toml                     |   2 
crates/picker/Cargo.toml                       |  18 
crates/picker/src/picker.rs                    | 507 +++++++++----------
crates/picker2/Cargo.toml                      |  28 -
crates/picker2/src/picker2.rs                  | 323 ------------
crates/project_symbols/Cargo.toml              |   2 
crates/recent_projects/Cargo.toml              |   2 
crates/storybook2/Cargo.toml                   |   2 
crates/theme_selector/Cargo.toml               |   2 
crates/vcs_menu/Cargo.toml                     |   2 
crates/welcome/Cargo.toml                      |   2 
crates/zed/Cargo.toml                          |   2 
crates/zed/src/app_menus.rs                    |   2 
32 files changed, 405 insertions(+), 1,728 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1784,7 +1784,7 @@ dependencies = [
  "db2",
  "editor2",
  "feature_flags2",
- "feedback2",
+ "feedback",
  "futures 0.3.28",
  "fuzzy2",
  "gpui2",
@@ -1793,7 +1793,7 @@ dependencies = [
  "log",
  "menu2",
  "notifications2",
- "picker2",
+ "picker",
  "postage",
  "pretty_assertions",
  "project2",
@@ -1859,7 +1859,7 @@ dependencies = [
  "gpui2",
  "language2",
  "menu2",
- "picker2",
+ "picker",
  "project2",
  "serde",
  "serde_json",
@@ -2976,36 +2976,6 @@ dependencies = [
 [[package]]
 name = "feedback"
 version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "editor",
- "futures 0.3.28",
- "gpui",
- "human_bytes",
- "isahc",
- "language",
- "lazy_static",
- "log",
- "postage",
- "project",
- "regex",
- "search",
- "serde",
- "serde_derive",
- "settings",
- "smallvec",
- "sysinfo",
- "theme",
- "tree-sitter-markdown",
- "urlencoding",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "feedback2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "bitflags 2.4.1",
@@ -3050,7 +3020,7 @@ dependencies = [
  "gpui2",
  "language2",
  "menu2",
- "picker2",
+ "picker",
  "postage",
  "project2",
  "serde",
@@ -4522,7 +4492,7 @@ dependencies = [
  "fuzzy2",
  "gpui2",
  "language2",
- "picker2",
+ "picker",
  "project2",
  "settings2",
  "theme2",
@@ -5850,7 +5820,7 @@ dependencies = [
  "gpui2",
  "language2",
  "ordered-float 2.10.0",
- "picker2",
+ "picker",
  "postage",
  "settings2",
  "smol",
@@ -6067,23 +6037,6 @@ dependencies = [
 [[package]]
 name = "picker"
 version = "0.1.0"
-dependencies = [
- "ctor",
- "editor",
- "env_logger",
- "gpui",
- "menu",
- "parking_lot 0.11.2",
- "serde_json",
- "settings",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "picker2"
-version = "0.1.0"
 dependencies = [
  "ctor",
  "editor2",
@@ -6540,7 +6493,7 @@ dependencies = [
  "language2",
  "lsp2",
  "ordered-float 2.10.0",
- "picker2",
+ "picker",
  "postage",
  "project2",
  "settings2",
@@ -6890,7 +6843,7 @@ dependencies = [
  "gpui2",
  "language2",
  "ordered-float 2.10.0",
- "picker2",
+ "picker",
  "postage",
  "settings2",
  "smol",
@@ -8660,7 +8613,7 @@ dependencies = [
  "language2",
  "log",
  "menu2",
- "picker2",
+ "picker",
  "rust-embed",
  "serde",
  "settings2",
@@ -9168,7 +9121,7 @@ dependencies = [
  "gpui2",
  "log",
  "parking_lot 0.11.2",
- "picker2",
+ "picker",
  "postage",
  "settings2",
  "smol",
@@ -10236,7 +10189,7 @@ dependencies = [
  "fs2",
  "fuzzy2",
  "gpui2",
- "picker2",
+ "picker",
  "ui2",
  "util",
  "workspace2",
@@ -10682,7 +10635,7 @@ dependencies = [
  "gpui2",
  "install_cli2",
  "log",
- "picker2",
+ "picker",
  "project2",
  "schemars",
  "serde",
@@ -11122,7 +11075,7 @@ dependencies = [
  "editor2",
  "env_logger",
  "feature_flags2",
- "feedback2",
+ "feedback",
  "file_finder",
  "fs2",
  "fsevent",

Cargo.toml 🔗

@@ -71,7 +71,6 @@ members = [
     "crates/outline",
     "crates/outline2",
     "crates/picker",
-    "crates/picker2",
     "crates/plugin",
     "crates/plugin_macros",
     "crates/prettier",

crates/collab_ui/Cargo.toml 🔗

@@ -32,14 +32,14 @@ collections = { path = "../collections" }
 # context_menu = { path = "../context_menu" }
 # drag_and_drop = { path = "../drag_and_drop" }
 editor = { package="editor2", path = "../editor2" }
-feedback = { package = "feedback2", path = "../feedback2" }
+feedback = { path = "../feedback" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
 language = { package = "language2", path = "../language2" }
 menu = { package = "menu2",  path = "../menu2" }
 notifications = { package = "notifications2",  path = "../notifications2" }
 rich_text = { package = "rich_text2", path = "../rich_text2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 project = { package = "project2", path = "../project2" }
 recent_projects = { path = "../recent_projects" }
 rpc = { package ="rpc2",  path = "../rpc2" }

crates/command_palette/Cargo.toml 🔗

@@ -13,7 +13,7 @@ collections = { path = "../collections" }
 editor = { package = "editor2", path = "../editor2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 project = { package = "project2", path = "../project2" }
 settings = { package = "settings2", path = "../settings2" }
 ui = { package = "ui2", path = "../ui2" }

crates/command_palette2/Cargo.toml 🔗

@@ -0,0 +1,36 @@
+[package]
+name = "command_palette"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/command_palette.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { path = "../picker" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+ui = { package = "ui2", path = "../ui2" }
+util = { path = "../util" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package="workspace2", path = "../workspace2" }
+zed_actions = { package = "zed_actions2", path = "../zed_actions2" }
+anyhow.workspace = true
+serde.workspace = true
+[dev-dependencies]
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+language = { package="language2", path = "../language2", features = ["test-support"] }
+project = { package="project2", path = "../project2", features = ["test-support"] }
+menu = { package = "menu2", path = "../menu2" }
+go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
+serde_json.workspace = true
+workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
+ctor.workspace = true
+env_logger.workspace = true

crates/feedback/Cargo.toml 🔗

@@ -11,31 +11,37 @@ path = "src/feedback.rs"
 test-support = []
 
 [dependencies]
-client = { path = "../client" }
-editor = { path = "../editor" }
-language = { path = "../language" }
-gpui = { path = "../gpui" }
-project = { path = "../project" }
-regex.workspace = true
+client = { package = "client2", path = "../client2" }
+db = { package = "db2", path = "../db2" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+menu = { package = "menu2", path = "../menu2" }
+project = { package = "project2", path = "../project2" }
 search = { path = "../search" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
 util = { path = "../util" }
-workspace = { path = "../workspace" }
+workspace = { package = "workspace2", path = "../workspace2"}
 
-log.workspace = true
-futures.workspace = true
-anyhow.workspace = true
-smallvec.workspace = true
+bitflags = "2.4.1"
 human_bytes = "0.4.1"
+
+anyhow.workspace = true
+futures.workspace = true
 isahc.workspace = true
 lazy_static.workspace = true
+log.workspace = true
 postage.workspace = true
+regex.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+smallvec.workspace = true
+smol.workspace = true
 sysinfo.workspace = true
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/feedback/src/deploy_feedback_button.rs 🔗

@@ -1,91 +1,49 @@
-use gpui::{
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    Entity, View, ViewContext, WeakViewHandle,
-};
+use gpui::{Render, ViewContext, WeakView};
+use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
-use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
+use crate::{feedback_modal::FeedbackModal, GiveFeedback};
 
 pub struct DeployFeedbackButton {
-    active: bool,
-    workspace: WeakViewHandle<Workspace>,
-}
-
-impl Entity for DeployFeedbackButton {
-    type Event = ();
+    workspace: WeakView<Workspace>,
 }
 
 impl DeployFeedbackButton {
     pub fn new(workspace: &Workspace) -> Self {
         DeployFeedbackButton {
-            active: false,
             workspace: workspace.weak_handle(),
         }
     }
 }
 
-impl View for DeployFeedbackButton {
-    fn ui_name() -> &'static str {
-        "DeployFeedbackButton"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let active = self.active;
-        let theme = theme::current(cx).clone();
-        Stack::new()
-            .with_child(
-                MouseEventHandler::new::<Self, _>(0, cx, |state, _| {
-                    let style = &theme
-                        .workspace
-                        .status_bar
-                        .panel_buttons
-                        .button
-                        .in_state(active)
-                        .style_for(state);
-
-                    Svg::new("icons/feedback.svg")
-                        .with_color(style.icon_color)
-                        .constrained()
-                        .with_width(style.icon_size)
-                        .aligned()
-                        .constrained()
-                        .with_width(style.icon_size)
-                        .with_height(style.icon_size)
-                        .contained()
-                        .with_style(style.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    if !active {
-                        if let Some(workspace) = this.workspace.upgrade(cx) {
-                            workspace
-                                .update(cx, |workspace, cx| FeedbackEditor::deploy(workspace, cx))
-                        }
-                    }
+impl Render for DeployFeedbackButton {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let is_open = self
+            .workspace
+            .upgrade()
+            .and_then(|workspace| {
+                workspace.update(cx, |workspace, cx| {
+                    workspace.active_modal::<FeedbackModal>(cx)
                 })
-                .with_tooltip::<Self>(
-                    0,
-                    "Send Feedback",
-                    Some(Box::new(GiveFeedback)),
-                    theme.tooltip.clone(),
-                    cx,
-                ),
-            )
-            .into_any()
+            })
+            .is_some();
+        IconButton::new("give-feedback", Icon::Envelope)
+            .style(ui::ButtonStyle::Subtle)
+            .icon_size(IconSize::Small)
+            .selected(is_open)
+            .tooltip(|cx| Tooltip::text("Share Feedback", cx))
+            .on_click(|_, cx| {
+                cx.dispatch_action(Box::new(GiveFeedback));
+            })
+            .into_any_element()
     }
 }
 
 impl StatusItemView for DeployFeedbackButton {
-    fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        if let Some(item) = item {
-            if let Some(_) = item.downcast::<FeedbackEditor>() {
-                self.active = true;
-                cx.notify();
-                return;
-            }
-        }
-        self.active = false;
-        cx.notify();
+    fn set_active_pane_item(
+        &mut self,
+        _item: Option<&dyn ItemHandle>,
+        _cx: &mut ViewContext<Self>,
+    ) {
     }
 }

crates/feedback/src/feedback.rs 🔗

@@ -1,12 +1,13 @@
+use gpui::{actions, AppContext, ClipboardItem, PromptLevel};
+use system_specs::SystemSpecs;
+use workspace::Workspace;
+
 pub mod deploy_feedback_button;
-pub mod feedback_editor;
-pub mod feedback_info_text;
-pub mod submit_feedback_button;
+pub mod feedback_modal;
+
+actions!(feedback, [GiveFeedback, SubmitFeedback]);
 
 mod system_specs;
-use gpui::{actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
-use system_specs::SystemSpecs;
-use workspace::Workspace;
 
 actions!(
     zed,
@@ -19,44 +20,42 @@ actions!(
 );
 
 pub fn init(cx: &mut AppContext) {
-    feedback_editor::init(cx);
-
-    cx.add_action(
-        move |_: &mut Workspace,
-              _: &CopySystemSpecsIntoClipboard,
-              cx: &mut ViewContext<Workspace>| {
-            let specs = SystemSpecs::new(&cx).to_string();
-            cx.prompt(
-                PromptLevel::Info,
-                &format!("Copied into clipboard:\n\n{specs}"),
-                &["OK"],
-            );
-            let item = ClipboardItem::new(specs.clone());
-            cx.write_to_clipboard(item);
-        },
-    );
-
-    cx.add_action(
-        |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
-            let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
-            cx.platform().open_url(url);
-        },
-    );
-
-    cx.add_action(
-        move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
-            let url = format!(
-                "https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
-                urlencoding::encode(&SystemSpecs::new(&cx).to_string())
-            );
-            cx.platform().open_url(&url);
-        },
-    );
-
-    cx.add_global_action(open_zed_community_repo);
-}
-
-pub fn open_zed_community_repo(_: &OpenZedCommunityRepo, cx: &mut AppContext) {
-    let url = "https://github.com/zed-industries/community";
-    cx.platform().open_url(&url);
+    // TODO: a way to combine these two into one?
+    cx.observe_new_views(feedback_modal::FeedbackModal::register)
+        .detach();
+
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace
+            .register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| {
+                    let specs = SystemSpecs::new(&cx).to_string();
+
+                    let prompt = cx.prompt(
+                        PromptLevel::Info,
+                        &format!("Copied into clipboard:\n\n{specs}"),
+                        &["OK"],
+                    );
+                    cx.spawn(|_, _cx| async move {
+                        prompt.await.ok();
+                    })
+                    .detach();
+                    let item = ClipboardItem::new(specs.clone());
+                    cx.write_to_clipboard(item);
+                })
+            .register_action(|_, _: &RequestFeature, cx| {
+                let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
+                cx.open_url(url);
+            })
+            .register_action(move |_, _: &FileBugReport, cx| {
+                let url = format!(
+                    "https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
+                    urlencoding::encode(&SystemSpecs::new(&cx).to_string())
+                );
+                cx.open_url(&url);
+            })
+            .register_action(move |_, _: &OpenZedCommunityRepo, cx| {
+                let url = "https://github.com/zed-industries/community";
+                cx.open_url(&url);
+        });
+    })
+    .detach();
 }

crates/feedback/src/feedback_editor.rs 🔗

@@ -1,442 +0,0 @@
-use crate::system_specs::SystemSpecs;
-use anyhow::bail;
-use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
-use editor::{Anchor, Editor};
-use futures::AsyncReadExt;
-use gpui::{
-    actions,
-    elements::{ChildView, Flex, Label, ParentElement, Svg},
-    platform::PromptLevel,
-    serde_json, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View,
-    ViewContext, ViewHandle,
-};
-use isahc::Request;
-use language::Buffer;
-use postage::prelude::Stream;
-use project::{search::SearchQuery, Project};
-use regex::Regex;
-use serde::Serialize;
-use smallvec::SmallVec;
-use std::{
-    any::TypeId,
-    borrow::Cow,
-    ops::{Range, RangeInclusive},
-    sync::Arc,
-};
-use util::ResultExt;
-use workspace::{
-    item::{Item, ItemEvent, ItemHandle},
-    searchable::{SearchableItem, SearchableItemHandle},
-    Workspace,
-};
-
-const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
-const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
-    "Feedback failed to submit, see error log for details.";
-
-actions!(feedback, [GiveFeedback, SubmitFeedback]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action({
-        move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
-            FeedbackEditor::deploy(workspace, cx);
-        }
-    });
-}
-
-#[derive(Serialize)]
-struct FeedbackRequestBody<'a> {
-    feedback_text: &'a str,
-    email: Option<String>,
-    metrics_id: Option<Arc<str>>,
-    installation_id: Option<Arc<str>>,
-    system_specs: SystemSpecs,
-    is_staff: bool,
-    token: &'a str,
-}
-
-#[derive(Clone)]
-pub(crate) struct FeedbackEditor {
-    system_specs: SystemSpecs,
-    editor: ViewHandle<Editor>,
-    project: ModelHandle<Project>,
-    pub allow_submission: bool,
-}
-
-impl FeedbackEditor {
-    fn new(
-        system_specs: SystemSpecs,
-        project: ModelHandle<Project>,
-        buffer: ModelHandle<Buffer>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let editor = cx.add_view(|cx| {
-            let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
-            editor.set_vertical_scroll_margin(5, cx);
-            editor
-        });
-
-        cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()))
-            .detach();
-
-        Self {
-            system_specs: system_specs.clone(),
-            editor,
-            project,
-            allow_submission: true,
-        }
-    }
-
-    pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
-        if !self.allow_submission {
-            return Task::ready(Ok(()));
-        }
-
-        let feedback_text = self.editor.read(cx).text(cx);
-        let feedback_char_count = feedback_text.chars().count();
-        let feedback_text = feedback_text.trim().to_string();
-
-        let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
-            Some(format!(
-                "Feedback can't be shorter than {} characters.",
-                FEEDBACK_CHAR_LIMIT.start()
-            ))
-        } else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() {
-            Some(format!(
-                "Feedback can't be longer than {} characters.",
-                FEEDBACK_CHAR_LIMIT.end()
-            ))
-        } else {
-            None
-        };
-
-        if let Some(error) = error {
-            cx.prompt(PromptLevel::Critical, &error, &["OK"]);
-            return Task::ready(Ok(()));
-        }
-
-        let mut answer = cx.prompt(
-            PromptLevel::Info,
-            "Ready to submit your feedback?",
-            &["Yes, Submit!", "No"],
-        );
-
-        let client = cx.global::<Arc<Client>>().clone();
-        let specs = self.system_specs.clone();
-
-        cx.spawn(|this, mut cx| async move {
-            let answer = answer.recv().await;
-
-            if answer == Some(0) {
-                this.update(&mut cx, |feedback_editor, cx| {
-                    feedback_editor.set_allow_submission(false, cx);
-                })
-                .log_err();
-
-                match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
-                    Ok(_) => {
-                        this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
-                            .log_err();
-                    }
-
-                    Err(error) => {
-                        log::error!("{}", error);
-                        this.update(&mut cx, |feedback_editor, cx| {
-                            cx.prompt(
-                                PromptLevel::Critical,
-                                FEEDBACK_SUBMISSION_ERROR_TEXT,
-                                &["OK"],
-                            );
-                            feedback_editor.set_allow_submission(true, cx);
-                        })
-                        .log_err();
-                    }
-                }
-            }
-        })
-        .detach();
-
-        Task::ready(Ok(()))
-    }
-
-    fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext<Self>) {
-        self.allow_submission = allow_submission;
-        cx.notify();
-    }
-
-    async fn submit_feedback(
-        feedback_text: &str,
-        zed_client: Arc<Client>,
-        system_specs: SystemSpecs,
-    ) -> anyhow::Result<()> {
-        let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
-
-        let telemetry = zed_client.telemetry();
-        let metrics_id = telemetry.metrics_id();
-        let installation_id = telemetry.installation_id();
-        let is_staff = telemetry.is_staff();
-        let http_client = zed_client.http_client();
-
-        let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
-
-        let emails: Vec<&str> = re
-            .captures_iter(feedback_text)
-            .map(|capture| capture.get(0).unwrap().as_str())
-            .collect();
-
-        let email = emails.first().map(|e| e.to_string());
-
-        let request = FeedbackRequestBody {
-            feedback_text: &feedback_text,
-            email,
-            metrics_id,
-            installation_id,
-            system_specs,
-            is_staff: is_staff.unwrap_or(false),
-            token: ZED_SECRET_CLIENT_TOKEN,
-        };
-
-        let json_bytes = serde_json::to_vec(&request)?;
-
-        let request = Request::post(feedback_endpoint)
-            .header("content-type", "application/json")
-            .body(json_bytes.into())?;
-
-        let mut response = http_client.send(request).await?;
-        let mut body = String::new();
-        response.body_mut().read_to_string(&mut body).await?;
-
-        let response_status = response.status();
-
-        if !response_status.is_success() {
-            bail!("Feedback API failed with error: {}", response_status)
-        }
-
-        Ok(())
-    }
-}
-
-impl FeedbackEditor {
-    pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
-        let markdown = workspace
-            .app_state()
-            .languages
-            .language_for_name("Markdown");
-        cx.spawn(|workspace, mut cx| async move {
-            let markdown = markdown.await.log_err();
-            workspace
-                .update(&mut cx, |workspace, cx| {
-                    workspace.with_local_workspace(cx, |workspace, cx| {
-                        let project = workspace.project().clone();
-                        let buffer = project
-                            .update(cx, |project, cx| project.create_buffer("", markdown, cx))
-                            .expect("creating buffers on a local workspace always succeeds");
-                        let system_specs = SystemSpecs::new(cx);
-                        let feedback_editor = cx
-                            .add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
-                        workspace.add_item(Box::new(feedback_editor), cx);
-                    })
-                })?
-                .await
-        })
-        .detach_and_log_err(cx);
-    }
-}
-
-impl View for FeedbackEditor {
-    fn ui_name() -> &'static str {
-        "FeedbackEditor"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        ChildView::new(&self.editor, cx).into_any()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.editor);
-        }
-    }
-}
-
-impl Entity for FeedbackEditor {
-    type Event = editor::Event;
-}
-
-impl Item for FeedbackEditor {
-    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
-        Some("Send Feedback".into())
-    }
-
-    fn tab_content<T: 'static>(
-        &self,
-        _: Option<usize>,
-        style: &theme::Tab,
-        _: &AppContext,
-    ) -> AnyElement<T> {
-        Flex::row()
-            .with_child(
-                Svg::new("icons/feedback.svg")
-                    .with_color(style.label.text.color)
-                    .constrained()
-                    .with_width(style.type_icon_width)
-                    .aligned()
-                    .contained()
-                    .with_margin_right(style.spacing),
-            )
-            .with_child(
-                Label::new("Send Feedback", style.label.clone())
-                    .aligned()
-                    .contained(),
-            )
-            .into_any()
-    }
-
-    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
-        self.editor.for_each_project_item(cx, f)
-    }
-
-    fn is_singleton(&self, _: &AppContext) -> bool {
-        true
-    }
-
-    fn can_save(&self, _: &AppContext) -> bool {
-        true
-    }
-
-    fn save(
-        &mut self,
-        _: ModelHandle<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<anyhow::Result<()>> {
-        self.submit(cx)
-    }
-
-    fn save_as(
-        &mut self,
-        _: ModelHandle<Project>,
-        _: std::path::PathBuf,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<anyhow::Result<()>> {
-        self.submit(cx)
-    }
-
-    fn reload(
-        &mut self,
-        _: ModelHandle<Project>,
-        _: &mut ViewContext<Self>,
-    ) -> Task<anyhow::Result<()>> {
-        Task::Ready(Some(Ok(())))
-    }
-
-    fn clone_on_split(
-        &self,
-        _workspace_id: workspace::WorkspaceId,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Self>
-    where
-        Self: Sized,
-    {
-        let buffer = self
-            .editor
-            .read(cx)
-            .buffer()
-            .read(cx)
-            .as_singleton()
-            .expect("Feedback buffer is only ever singleton");
-
-        Some(Self::new(
-            self.system_specs.clone(),
-            self.project.clone(),
-            buffer.clone(),
-            cx,
-        ))
-    }
-
-    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
-        Some(Box::new(handle.clone()))
-    }
-
-    fn act_as_type<'a>(
-        &'a self,
-        type_id: TypeId,
-        self_handle: &'a ViewHandle<Self>,
-        _: &'a AppContext,
-    ) -> Option<&'a AnyViewHandle> {
-        if type_id == TypeId::of::<Self>() {
-            Some(self_handle)
-        } else if type_id == TypeId::of::<Editor>() {
-            Some(&self.editor)
-        } else {
-            None
-        }
-    }
-
-    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
-        Editor::to_item_events(event)
-    }
-}
-
-impl SearchableItem for FeedbackEditor {
-    type Match = Range<Anchor>;
-
-    fn to_search_event(
-        &mut self,
-        event: &Self::Event,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<workspace::searchable::SearchEvent> {
-        self.editor
-            .update(cx, |editor, cx| editor.to_search_event(event, cx))
-    }
-
-    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| editor.clear_matches(cx))
-    }
-
-    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| editor.update_matches(matches, cx))
-    }
-
-    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
-        self.editor
-            .update(cx, |editor, cx| editor.query_suggestion(cx))
-    }
-
-    fn activate_match(
-        &mut self,
-        index: usize,
-        matches: Vec<Self::Match>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.editor
-            .update(cx, |editor, cx| editor.activate_match(index, matches, cx))
-    }
-
-    fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |e, cx| e.select_matches(matches, cx))
-    }
-    fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |e, cx| e.replace(matches, query, cx));
-    }
-    fn find_matches(
-        &mut self,
-        query: Arc<project::search::SearchQuery>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Vec<Self::Match>> {
-        self.editor
-            .update(cx, |editor, cx| editor.find_matches(query, cx))
-    }
-
-    fn active_match_index(
-        &mut self,
-        matches: Vec<Self::Match>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<usize> {
-        self.editor
-            .update(cx, |editor, cx| editor.active_match_index(matches, cx))
-    }
-}

crates/feedback/src/feedback_info_text.rs 🔗

@@ -1,94 +0,0 @@
-use gpui::{
-    elements::{Flex, Label, MouseEventHandler, ParentElement, Text},
-    platform::{CursorStyle, MouseButton},
-    AnyElement, Element, Entity, View, ViewContext, ViewHandle,
-};
-use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
-
-use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
-
-pub struct FeedbackInfoText {
-    active_item: Option<ViewHandle<FeedbackEditor>>,
-}
-
-impl FeedbackInfoText {
-    pub fn new() -> Self {
-        Self {
-            active_item: Default::default(),
-        }
-    }
-}
-
-impl Entity for FeedbackInfoText {
-    type Event = ();
-}
-
-impl View for FeedbackInfoText {
-    fn ui_name() -> &'static str {
-        "FeedbackInfoText"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx).clone();
-
-        Flex::row()
-            .with_child(
-                Text::new(
-                    "Share your feedback. Include your email for replies. For issues and discussions, visit the ",
-                    theme.feedback.info_text_default.text.clone(),
-                )
-                .with_soft_wrap(false)
-                .aligned(),
-            )
-            .with_child(
-                MouseEventHandler::new::<OpenZedCommunityRepo, _>(0, cx, |state, _| {
-                    let style = if state.hovered() {
-                        &theme.feedback.link_text_hover
-                    } else {
-                        &theme.feedback.link_text_default
-                    };
-                    Label::new("community repo", style.text.clone())
-                        .contained()
-                        .with_style(style.container)
-                        .aligned()
-                        .left()
-                        .clipped()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, _, cx| {
-                    open_zed_community_repo(&Default::default(), cx)
-                }),
-            )
-            .with_child(
-                Text::new(".", theme.feedback.info_text_default.text.clone())
-                    .with_soft_wrap(false)
-                    .aligned(),
-            )
-            .contained()
-            .with_style(theme.feedback.info_text_default.container)
-            .aligned()
-            .left()
-            .clipped()
-            .into_any()
-    }
-}
-
-impl ToolbarItemView for FeedbackInfoText {
-    fn set_active_pane_item(
-        &mut self,
-        active_pane_item: Option<&dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
-    ) -> workspace::ToolbarItemLocation {
-        cx.notify();
-        if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
-        {
-            self.active_item = Some(feedback_editor);
-            ToolbarItemLocation::PrimaryLeft {
-                flex: Some((1., false)),
-            }
-        } else {
-            self.active_item = None;
-            ToolbarItemLocation::Hidden
-        }
-    }
-}

crates/feedback/src/submit_feedback_button.rs 🔗

@@ -1,108 +0,0 @@
-use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
-use anyhow::Result;
-use gpui::{
-    elements::{Label, MouseEventHandler},
-    platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
-};
-use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_async_action(SubmitFeedbackButton::submit);
-}
-
-pub struct SubmitFeedbackButton {
-    pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
-}
-
-impl SubmitFeedbackButton {
-    pub fn new() -> Self {
-        Self {
-            active_item: Default::default(),
-        }
-    }
-
-    pub fn submit(
-        &mut self,
-        _: &SubmitFeedback,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Task<Result<()>>> {
-        if let Some(active_item) = self.active_item.as_ref() {
-            Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.submit(cx)))
-        } else {
-            None
-        }
-    }
-}
-
-impl Entity for SubmitFeedbackButton {
-    type Event = ();
-}
-
-impl View for SubmitFeedbackButton {
-    fn ui_name() -> &'static str {
-        "SubmitFeedbackButton"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx).clone();
-        let allow_submission = self
-            .active_item
-            .as_ref()
-            .map_or(true, |i| i.read(cx).allow_submission);
-
-        enum SubmitFeedbackButton {}
-        MouseEventHandler::new::<SubmitFeedbackButton, _>(0, cx, |state, _| {
-            let text;
-            let style = if allow_submission {
-                text = "Submit as Markdown";
-                theme.feedback.submit_button.style_for(state)
-            } else {
-                text = "Submitting...";
-                theme
-                    .feedback
-                    .submit_button
-                    .disabled
-                    .as_ref()
-                    .unwrap_or(&theme.feedback.submit_button.default)
-            };
-
-            Label::new(text, style.text.clone())
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, |_, this, cx| {
-            this.submit(&Default::default(), cx);
-        })
-        .aligned()
-        .contained()
-        .with_margin_left(theme.feedback.button_margin)
-        .with_tooltip::<Self>(
-            0,
-            "cmd-s",
-            Some(Box::new(SubmitFeedback)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .into_any()
-    }
-}
-
-impl ToolbarItemView for SubmitFeedbackButton {
-    fn set_active_pane_item(
-        &mut self,
-        active_pane_item: Option<&dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
-    ) -> workspace::ToolbarItemLocation {
-        cx.notify();
-        if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
-        {
-            self.active_item = Some(feedback_editor);
-            ToolbarItemLocation::PrimaryRight { flex: None }
-        } else {
-            self.active_item = None;
-            ToolbarItemLocation::Hidden
-        }
-    }
-}

crates/feedback/src/system_specs.rs 🔗

@@ -1,17 +1,14 @@
 use client::ZED_APP_VERSION;
-use gpui::{platform::AppVersion, AppContext};
+use gpui::AppContext;
 use human_bytes::human_bytes;
 use serde::Serialize;
 use std::{env, fmt::Display};
 use sysinfo::{System, SystemExt};
 use util::channel::ReleaseChannel;
 
-// TODO: Move this file out of feedback and into a more general place
-
 #[derive(Clone, Debug, Serialize)]
 pub struct SystemSpecs {
-    #[serde(serialize_with = "serialize_app_version")]
-    app_version: Option<AppVersion>,
+    app_version: Option<String>,
     release_channel: &'static str,
     os_name: &'static str,
     os_version: Option<String>,
@@ -21,16 +18,17 @@ pub struct SystemSpecs {
 
 impl SystemSpecs {
     pub fn new(cx: &AppContext) -> Self {
-        let platform = cx.platform();
-        let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok());
+        let app_version = ZED_APP_VERSION
+            .or_else(|| cx.app_metadata().app_version)
+            .map(|v| v.to_string());
         let release_channel = cx.global::<ReleaseChannel>().display_name();
-        let os_name = platform.os_name();
+        let os_name = cx.app_metadata().os_name;
         let system = System::new_all();
         let memory = system.total_memory();
         let architecture = env::consts::ARCH;
-        let os_version = platform
-            .os_version()
-            .ok()
+        let os_version = cx
+            .app_metadata()
+            .os_version
             .map(|os_version| os_version.to_string());
 
         SystemSpecs {
@@ -68,10 +66,3 @@ impl Display for SystemSpecs {
         write!(f, "{system_specs}")
     }
 }
-
-fn serialize_app_version<S>(version: &Option<AppVersion>, serializer: S) -> Result<S::Ok, S::Error>
-where
-    S: serde::Serializer,
-{
-    version.map(|v| v.to_string()).serialize(serializer)
-}

crates/feedback2/Cargo.toml 🔗

@@ -1,47 +0,0 @@
-[package]
-name = "feedback2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/feedback2.rs"
-
-[features]
-test-support = []
-
-[dependencies]
-client = { package = "client2", path = "../client2" }
-db = { package = "db2", path = "../db2" }
-editor = { package = "editor2", path = "../editor2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-language = { package = "language2", path = "../language2" }
-menu = { package = "menu2", path = "../menu2" }
-project = { package = "project2", path = "../project2" }
-search = { path = "../search" }
-settings = { package = "settings2", path = "../settings2" }
-theme = { package = "theme2", path = "../theme2" }
-ui = { package = "ui2", path = "../ui2" }
-util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2"}
-
-bitflags = "2.4.1"
-human_bytes = "0.4.1"
-
-anyhow.workspace = true
-futures.workspace = true
-isahc.workspace = true
-lazy_static.workspace = true
-log.workspace = true
-postage.workspace = true
-regex.workspace = true
-serde.workspace = true
-serde_derive.workspace = true
-smallvec.workspace = true
-smol.workspace = true
-sysinfo.workspace = true
-tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
-urlencoding = "2.1.2"
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/feedback2/src/deploy_feedback_button.rs 🔗

@@ -1,49 +0,0 @@
-use gpui::{Render, ViewContext, WeakView};
-use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
-use workspace::{item::ItemHandle, StatusItemView, Workspace};
-
-use crate::{feedback_modal::FeedbackModal, GiveFeedback};
-
-pub struct DeployFeedbackButton {
-    workspace: WeakView<Workspace>,
-}
-
-impl DeployFeedbackButton {
-    pub fn new(workspace: &Workspace) -> Self {
-        DeployFeedbackButton {
-            workspace: workspace.weak_handle(),
-        }
-    }
-}
-
-impl Render for DeployFeedbackButton {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let is_open = self
-            .workspace
-            .upgrade()
-            .and_then(|workspace| {
-                workspace.update(cx, |workspace, cx| {
-                    workspace.active_modal::<FeedbackModal>(cx)
-                })
-            })
-            .is_some();
-        IconButton::new("give-feedback", Icon::Envelope)
-            .style(ui::ButtonStyle::Subtle)
-            .icon_size(IconSize::Small)
-            .selected(is_open)
-            .tooltip(|cx| Tooltip::text("Share Feedback", cx))
-            .on_click(|_, cx| {
-                cx.dispatch_action(Box::new(GiveFeedback));
-            })
-            .into_any_element()
-    }
-}
-
-impl StatusItemView for DeployFeedbackButton {
-    fn set_active_pane_item(
-        &mut self,
-        _item: Option<&dyn ItemHandle>,
-        _cx: &mut ViewContext<Self>,
-    ) {
-    }
-}

crates/feedback2/src/feedback2.rs 🔗

@@ -1,61 +0,0 @@
-use gpui::{actions, AppContext, ClipboardItem, PromptLevel};
-use system_specs::SystemSpecs;
-use workspace::Workspace;
-
-pub mod deploy_feedback_button;
-pub mod feedback_modal;
-
-actions!(feedback, [GiveFeedback, SubmitFeedback]);
-
-mod system_specs;
-
-actions!(
-    zed,
-    [
-        CopySystemSpecsIntoClipboard,
-        FileBugReport,
-        RequestFeature,
-        OpenZedCommunityRepo
-    ]
-);
-
-pub fn init(cx: &mut AppContext) {
-    // TODO: a way to combine these two into one?
-    cx.observe_new_views(feedback_modal::FeedbackModal::register)
-        .detach();
-
-    cx.observe_new_views(|workspace: &mut Workspace, _| {
-        workspace
-            .register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| {
-                    let specs = SystemSpecs::new(&cx).to_string();
-
-                    let prompt = cx.prompt(
-                        PromptLevel::Info,
-                        &format!("Copied into clipboard:\n\n{specs}"),
-                        &["OK"],
-                    );
-                    cx.spawn(|_, _cx| async move {
-                        prompt.await.ok();
-                    })
-                    .detach();
-                    let item = ClipboardItem::new(specs.clone());
-                    cx.write_to_clipboard(item);
-                })
-            .register_action(|_, _: &RequestFeature, cx| {
-                let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
-                cx.open_url(url);
-            })
-            .register_action(move |_, _: &FileBugReport, cx| {
-                let url = format!(
-                    "https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
-                    urlencoding::encode(&SystemSpecs::new(&cx).to_string())
-                );
-                cx.open_url(&url);
-            })
-            .register_action(move |_, _: &OpenZedCommunityRepo, cx| {
-                let url = "https://github.com/zed-industries/community";
-                cx.open_url(&url);
-        });
-    })
-    .detach();
-}

crates/feedback2/src/system_specs.rs 🔗

@@ -1,68 +0,0 @@
-use client::ZED_APP_VERSION;
-use gpui::AppContext;
-use human_bytes::human_bytes;
-use serde::Serialize;
-use std::{env, fmt::Display};
-use sysinfo::{System, SystemExt};
-use util::channel::ReleaseChannel;
-
-#[derive(Clone, Debug, Serialize)]
-pub struct SystemSpecs {
-    app_version: Option<String>,
-    release_channel: &'static str,
-    os_name: &'static str,
-    os_version: Option<String>,
-    memory: u64,
-    architecture: &'static str,
-}
-
-impl SystemSpecs {
-    pub fn new(cx: &AppContext) -> Self {
-        let app_version = ZED_APP_VERSION
-            .or_else(|| cx.app_metadata().app_version)
-            .map(|v| v.to_string());
-        let release_channel = cx.global::<ReleaseChannel>().display_name();
-        let os_name = cx.app_metadata().os_name;
-        let system = System::new_all();
-        let memory = system.total_memory();
-        let architecture = env::consts::ARCH;
-        let os_version = cx
-            .app_metadata()
-            .os_version
-            .map(|os_version| os_version.to_string());
-
-        SystemSpecs {
-            app_version,
-            release_channel,
-            os_name,
-            os_version,
-            memory,
-            architecture,
-        }
-    }
-}
-
-impl Display for SystemSpecs {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let os_information = match &self.os_version {
-            Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
-            None => format!("OS: {}", self.os_name),
-        };
-        let app_version_information = self
-            .app_version
-            .as_ref()
-            .map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
-        let system_specs = [
-            app_version_information,
-            Some(os_information),
-            Some(format!("Memory: {}", human_bytes(self.memory as f64))),
-            Some(format!("Architecture: {}", self.architecture)),
-        ]
-        .into_iter()
-        .flatten()
-        .collect::<Vec<String>>()
-        .join("\n");
-
-        write!(f, "{system_specs}")
-    }
-}

crates/file_finder/Cargo.toml 🔗

@@ -14,7 +14,7 @@ collections = { path = "../collections" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
 menu = { package = "menu2", path = "../menu2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 project = { package = "project2", path = "../project2" }
 settings = { package = "settings2", path = "../settings2" }
 text = { package = "text2", path = "../text2" }

crates/language_selector/Cargo.toml 🔗

@@ -13,7 +13,7 @@ editor = { package = "editor2", path = "../editor2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 language = { package = "language2", path = "../language2" }
 gpui = { package = "gpui2", path = "../gpui2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 project = { package = "project2", path = "../project2" }
 theme = { package = "theme2", path = "../theme2" }
 ui = { package = "ui2", path = "../ui2" }

crates/outline2/Cargo.toml 🔗

@@ -14,7 +14,7 @@ fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
 ui = { package = "ui2", path = "../ui2" }
 language = { package = "language2", path = "../language2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 settings = { package = "settings2", path = "../settings2" }
 text = { package = "text2", path = "../text2" }
 theme = { package = "theme2", path = "../theme2" }

crates/picker/Cargo.toml 🔗

@@ -9,20 +9,20 @@ path = "src/picker.rs"
 doctest = false
 
 [dependencies]
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-menu = { path = "../menu" }
-settings = { path = "../settings" }
+editor = { package = "editor2", path = "../editor2" }
+ui = { package = "ui2", path = "../ui2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+menu = { package = "menu2", path = "../menu2" }
+settings = { package = "settings2", path = "../settings2" }
 util = { path = "../util" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2"}
 
 parking_lot.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
 serde_json.workspace = true
-workspace = { path = "../workspace", features = ["test-support"] }
 ctor.workspace = true
 env_logger.workspace = true

crates/picker/src/picker.rs 🔗

@@ -1,267 +1,204 @@
 use editor::Editor;
 use gpui::{
-    elements::*,
-    geometry::vector::{vec2f, Vector2F},
-    keymap_matcher::KeymapContext,
-    platform::{CursorStyle, MouseButton},
-    AnyElement, AnyViewHandle, AppContext, Axis, Entity, MouseState, Task, View, ViewContext,
-    ViewHandle,
+    div, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
+    FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle,
+    View, ViewContext, WindowContext,
 };
-use menu::{Cancel, Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
-use parking_lot::Mutex;
 use std::{cmp, sync::Arc};
-use util::ResultExt;
-use workspace::Modal;
-
-#[derive(Clone, Copy)]
-pub enum PickerEvent {
-    Dismiss,
-}
+use ui::{prelude::*, v_stack, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator};
+use workspace::ModalView;
 
 pub struct Picker<D: PickerDelegate> {
-    delegate: D,
-    query_editor: ViewHandle<Editor>,
-    list_state: UniformListState,
-    max_size: Vector2F,
-    theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
-    confirmed: bool,
-    pending_update_matches: Option<Task<Option<()>>>,
+    pub delegate: D,
+    scroll_handle: UniformListScrollHandle,
+    editor: View<Editor>,
+    pending_update_matches: Option<Task<()>>,
     confirm_on_update: Option<bool>,
-    has_focus: bool,
+    width: Option<Length>,
+    max_height: Option<Length>,
+
+    /// Whether the `Picker` is rendered as a self-contained modal.
+    ///
+    /// Set this to `false` when rendering the `Picker` as part of a larger modal.
+    is_modal: bool,
 }
 
 pub trait PickerDelegate: Sized + 'static {
-    fn placeholder_text(&self) -> Arc<str>;
+    type ListItem: IntoElement;
     fn match_count(&self) -> usize;
     fn selected_index(&self) -> usize;
+    fn separators_after_indices(&self) -> Vec<usize> {
+        Vec::new()
+    }
     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, cx: &mut ViewContext<Picker<Self>>);
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
+
     fn render_match(
         &self,
         ix: usize,
-        state: &mut MouseState,
         selected: bool,
-        cx: &AppContext,
-    ) -> AnyElement<Picker<Self>>;
-    fn center_selection_after_match_updates(&self) -> bool {
-        false
-    }
-    fn render_header(
-        &self,
-        _cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<AnyElement<Picker<Self>>> {
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem>;
+    fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
         None
     }
-    fn render_footer(
-        &self,
-        _cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<AnyElement<Picker<Self>>> {
+    fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
         None
     }
 }
 
-impl<D: PickerDelegate> Entity for Picker<D> {
-    type Event = PickerEvent;
-}
-
-impl<D: PickerDelegate> View for Picker<D> {
-    fn ui_name() -> &'static str {
-        "Picker"
+impl<D: PickerDelegate> FocusableView for Picker<D> {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.editor.focus_handle(cx)
     }
+}
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = (self.theme.lock())(theme::current(cx).as_ref());
-        let query = self.query(cx);
-        let match_count = self.delegate.match_count();
-
-        let container_style;
-        let editor_style;
-        if query.is_empty() && match_count == 0 {
-            container_style = theme.empty_container;
-            editor_style = theme.empty_input_editor.container;
-        } else {
-            container_style = theme.container;
-            editor_style = theme.input_editor.container;
+impl<D: PickerDelegate> Picker<D> {
+    pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
+        let editor = cx.new_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();
+        let mut this = Self {
+            delegate,
+            editor,
+            scroll_handle: UniformListScrollHandle::new(),
+            pending_update_matches: None,
+            confirm_on_update: None,
+            width: None,
+            max_height: None,
+            is_modal: true,
         };
-
-        Flex::new(Axis::Vertical)
-            .with_child(
-                ChildView::new(&self.query_editor, cx)
-                    .contained()
-                    .with_style(editor_style),
-            )
-            .with_children(self.delegate.render_header(cx))
-            .with_children(if match_count == 0 {
-                if query.is_empty() {
-                    None
-                } else {
-                    Some(
-                        Label::new("No matches", theme.no_matches.label.clone())
-                            .contained()
-                            .with_style(theme.no_matches.container)
-                            .into_any(),
-                    )
-                }
-            } else {
-                Some(
-                    UniformList::new(
-                        self.list_state.clone(),
-                        match_count,
-                        cx,
-                        move |this, mut range, items, cx| {
-                            let selected_ix = this.delegate.selected_index();
-                            range.end = cmp::min(range.end, this.delegate.match_count());
-                            items.extend(range.map(move |ix| {
-                                MouseEventHandler::new::<D, _>(ix, cx, |state, cx| {
-                                    this.delegate.render_match(ix, state, ix == selected_ix, cx)
-                                })
-                                // Capture mouse events
-                                .on_down(MouseButton::Left, |_, _, _| {})
-                                .on_up(MouseButton::Left, |_, _, _| {})
-                                .on_click(MouseButton::Left, move |click, picker, cx| {
-                                    picker.select_index(ix, click.cmd, cx);
-                                })
-                                .with_cursor_style(CursorStyle::PointingHand)
-                                .into_any()
-                            }));
-                        },
-                    )
-                    .contained()
-                    .with_margin_top(6.0)
-                    .flex(1., false)
-                    .into_any(),
-                )
-            })
-            .with_children(self.delegate.render_footer(cx))
-            .contained()
-            .with_style(container_style)
-            .constrained()
-            .with_max_width(self.max_size.x())
-            .with_max_height(self.max_size.y())
-            .into_any_named("picker")
+        this.update_matches("".to_string(), cx);
+        this
     }
 
-    fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
-        Self::reset_to_default_keymap_context(keymap);
-        keymap.add_identifier("menu");
+    pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
+        self.width = Some(width.into());
+        self
     }
 
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_focus = true;
-        if cx.is_self_focused() {
-            cx.focus(&self.query_editor);
-        }
+    pub fn max_height(mut self, max_height: impl Into<gpui::Length>) -> Self {
+        self.max_height = Some(max_height.into());
+        self
     }
 
-    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
+    pub fn modal(mut self, modal: bool) -> Self {
+        self.is_modal = modal;
+        self
     }
-}
 
-impl<D: PickerDelegate> Modal for Picker<D> {
-    fn has_focus(&self) -> bool {
-        self.has_focus
+    pub fn focus(&self, cx: &mut WindowContext) {
+        self.editor.update(cx, |editor, cx| editor.focus(cx));
     }
 
-    fn dismiss_on_event(event: &Self::Event) -> bool {
-        matches!(event, PickerEvent::Dismiss)
+    pub 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);
+            cx.notify();
+        }
     }
-}
 
-impl<D: PickerDelegate> Picker<D> {
-    pub fn init(cx: &mut AppContext) {
-        cx.add_action(Self::select_first);
-        cx.add_action(Self::select_last);
-        cx.add_action(Self::select_next);
-        cx.add_action(Self::select_prev);
-        cx.add_action(Self::confirm);
-        cx.add_action(Self::secondary_confirm);
-        cx.add_action(Self::cancel);
+    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);
+            cx.notify();
+        }
     }
 
-    pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
-        let theme = Arc::new(Mutex::new(
-            Box::new(|theme: &theme::Theme| theme.picker.clone())
-                as Box<dyn Fn(&theme::Theme) -> theme::Picker>,
-        ));
-        let placeholder_text = delegate.placeholder_text();
-        let query_editor = cx.add_view({
-            let picker_theme = theme.clone();
-            |cx| {
-                let mut editor = Editor::single_line(
-                    Some(Arc::new(move |theme| {
-                        (picker_theme.lock())(theme).input_editor.clone()
-                    })),
-                    cx,
-                );
-                editor.set_placeholder_text(placeholder_text, cx);
-                editor
-            }
-        });
-        cx.subscribe(&query_editor, Self::on_query_editor_event)
-            .detach();
-        let mut this = Self {
-            query_editor,
-            list_state: Default::default(),
-            delegate,
-            max_size: vec2f(540., 420.),
-            theme,
-            confirmed: false,
-            pending_update_matches: None,
-            confirm_on_update: None,
-            has_focus: false,
-        };
-        this.update_matches(String::new(), cx);
-        this
+    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);
+            cx.notify();
+        }
     }
 
-    pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
-        self.max_size = vec2f(width, height);
-        self
+    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);
+            cx.notify();
+        }
     }
 
-    pub fn with_theme<F>(self, theme: F) -> Self
-    where
-        F: 'static + Fn(&theme::Theme) -> theme::Picker,
-    {
-        *self.theme.lock() = Box::new(theme);
-        self
+    pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        let index = self.delegate.selected_index();
+        let new_index = if index + 1 == count { 0 } else { index + 1 };
+        self.delegate.set_selected_index(new_index, cx);
+        self.scroll_handle.scroll_to_item(new_index);
+        cx.notify();
     }
 
-    pub fn delegate(&self) -> &D {
-        &self.delegate
+    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        self.delegate.dismissed(cx);
+        cx.emit(DismissEvent);
     }
 
-    pub fn delegate_mut(&mut self) -> &mut D {
-        &mut self.delegate
+    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(false)
+        } else {
+            self.delegate.confirm(false, cx);
+        }
     }
 
-    pub fn query(&self, cx: &AppContext) -> String {
-        self.query_editor.read(cx).text(cx)
+    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(true)
+        } else {
+            self.delegate.confirm(true, cx);
+        }
     }
 
-    pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
-        self.query_editor
-            .update(cx, |editor, cx| editor.set_text(query, cx));
+    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
+        cx.stop_propagation();
+        cx.prevent_default();
+        self.delegate.set_selected_index(ix, cx);
+        self.delegate.confirm(secondary, cx);
     }
 
-    fn on_query_editor_event(
+    fn on_input_editor_event(
         &mut self,
-        _: ViewHandle<Editor>,
-        event: &editor::Event,
+        _: View<Editor>,
+        event: &editor::EditorEvent,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx),
-            editor::Event::Blurred if !self.confirmed => {
-                self.dismiss(cx);
+            editor::EditorEvent::BufferEdited => {
+                let query = self.editor.read(cx).text(cx);
+                self.update_matches(query, cx);
+            }
+            editor::EditorEvent::Blurred => {
+                self.cancel(&menu::Cancel, cx);
             }
             _ => {}
         }
     }
 
+    pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
+        let query = self.editor.read(cx).text(cx);
+        self.update_matches(query, cx);
+    }
+
     pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
         let update = self.delegate.update_matches(query, cx);
         self.matches_updated(cx);
@@ -270,99 +207,117 @@ impl<D: PickerDelegate> Picker<D> {
             this.update(&mut cx, |this, cx| {
                 this.matches_updated(cx);
             })
-            .log_err()
+            .ok();
         }));
     }
 
     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);
+        self.scroll_handle.scroll_to_item(index);
         self.pending_update_matches = None;
         if let Some(secondary) = self.confirm_on_update.take() {
-            self.confirmed = true;
-            self.delegate.confirm(secondary, cx)
+            self.delegate.confirm(secondary, cx);
         }
         cx.notify();
     }
 
-    pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        if self.delegate.match_count() > 0 {
-            self.delegate.set_selected_index(0, cx);
-            self.list_state.scroll_to(ScrollTarget::Show(0));
-        }
-
-        cx.notify();
-    }
-
-    pub fn select_index(&mut self, index: usize, cmd: bool, cx: &mut ViewContext<Self>) {
-        if self.delegate.match_count() > 0 {
-            self.confirmed = true;
-            self.delegate.set_selected_index(index, cx);
-            self.delegate.confirm(cmd, cx);
-        }
-    }
-
-    pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
-        let match_count = self.delegate.match_count();
-        if match_count > 0 {
-            let index = match_count - 1;
-            self.delegate.set_selected_index(index, cx);
-            self.list_state.scroll_to(ScrollTarget::Show(index));
-        }
-        cx.notify();
-    }
-
-    pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        let next_index = self.delegate.selected_index() + 1;
-        if next_index < self.delegate.match_count() {
-            self.delegate.set_selected_index(next_index, cx);
-            self.list_state.scroll_to(ScrollTarget::Show(next_index));
-        }
-
-        cx.notify();
-    }
-
-    pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        let mut selected_index = self.delegate.selected_index();
-        if selected_index > 0 {
-            selected_index -= 1;
-            self.delegate.set_selected_index(selected_index, cx);
-            self.list_state
-                .scroll_to(ScrollTarget::Show(selected_index));
-        }
-
-        cx.notify();
-    }
-
-    pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if self.pending_update_matches.is_some() {
-            self.confirm_on_update = Some(false)
-        } else {
-            self.confirmed = true;
-            self.delegate.confirm(false, cx);
-        }
+    pub fn query(&self, cx: &AppContext) -> String {
+        self.editor.read(cx).text(cx)
     }
 
-    pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        if self.pending_update_matches.is_some() {
-            self.confirm_on_update = Some(true)
-        } else {
-            self.confirmed = true;
-            self.delegate.confirm(true, cx);
-        }
+    pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |editor, cx| editor.set_text(query, cx));
     }
+}
 
-    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        self.dismiss(cx);
-    }
+impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
+impl<D: PickerDelegate> ModalView for Picker<D> {}
+
+impl<D: PickerDelegate> Render for Picker<D> {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let picker_editor = h_stack()
+            .overflow_hidden()
+            .flex_none()
+            .h_9()
+            .px_4()
+            .child(self.editor.clone());
+
+        div()
+            .key_context("Picker")
+            .size_full()
+            .when_some(self.width, |el, width| el.w(width))
+            .overflow_hidden()
+            // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
+            // as a part of a modal rather than the entire modal.
+            //
+            // We should revisit how the `Picker` is styled to make it more composable.
+            .when(self.is_modal, |this| this.elevation_3(cx))
+            .on_action(cx.listener(Self::select_next))
+            .on_action(cx.listener(Self::select_prev))
+            .on_action(cx.listener(Self::select_first))
+            .on_action(cx.listener(Self::select_last))
+            .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::secondary_confirm))
+            .child(picker_editor)
+            .child(Divider::horizontal())
+            .when(self.delegate.match_count() > 0, |el| {
+                el.child(
+                    v_stack()
+                        .flex_grow()
+                        .py_2()
+                        .max_h(self.max_height.unwrap_or(rems(18.).into()))
+                        .overflow_hidden()
+                        .children(self.delegate.render_header(cx))
+                        .child(
+                            uniform_list(
+                                cx.view().clone(),
+                                "candidates",
+                                self.delegate.match_count(),
+                                {
+                                    let separators_after_indices = self.delegate.separators_after_indices();
+                                    let selected_index = self.delegate.selected_index();
+                                    move |picker, visible_range, cx| {
+                                        visible_range
+                                            .map(|ix| {
+                                                div()
+                                                    .on_mouse_down(
+                                                        MouseButton::Left,
+                                                        cx.listener(move |this, event: &MouseDownEvent, cx| {
+                                                            this.handle_click(
+                                                                ix,
+                                                                event.modifiers.command,
+                                                                cx,
+                                                            )
+                                                        }),
+                                                    )
+                                                    .children(picker.delegate.render_match(
+                                                        ix,
+                                                        ix == selected_index,
+                                                        cx,
+                                                    )).when(separators_after_indices.contains(&ix), |picker| picker.child(ListSeparator))
+                                            })
+                                            .collect()
+                                    }
+                                },
+                            )
+                            .track_scroll(self.scroll_handle.clone())
+                        )
 
-    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
-        cx.emit(PickerEvent::Dismiss);
-        self.delegate.dismissed(cx);
+                )
+            })
+            .when(self.delegate.match_count() == 0, |el| {
+                el.child(
+                    v_stack().flex_grow().py_2().child(
+                        ListItem::new("empty_state")
+                            .inset(true)
+                            .spacing(ListItemSpacing::Sparse)
+                            .disabled(true)
+                            .child(Label::new("No matches").color(Color::Muted)),
+                    ),
+                )
+            })
+            .children(self.delegate.render_footer(cx))
     }
 }

crates/picker2/Cargo.toml 🔗

@@ -1,28 +0,0 @@
-[package]
-name = "picker2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/picker2.rs"
-doctest = false
-
-[dependencies]
-editor = { package = "editor2", path = "../editor2" }
-ui = { package = "ui2", path = "../ui2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-menu = { package = "menu2", path = "../menu2" }
-settings = { package = "settings2", path = "../settings2" }
-util = { path = "../util" }
-theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2"}
-
-parking_lot.workspace = true
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
-gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-serde_json.workspace = true
-ctor.workspace = true
-env_logger.workspace = true

crates/picker2/src/picker2.rs 🔗

@@ -1,323 +0,0 @@
-use editor::Editor;
-use gpui::{
-    div, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
-    FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle,
-    View, ViewContext, WindowContext,
-};
-use std::{cmp, sync::Arc};
-use ui::{prelude::*, v_stack, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator};
-use workspace::ModalView;
-
-pub struct Picker<D: PickerDelegate> {
-    pub delegate: D,
-    scroll_handle: UniformListScrollHandle,
-    editor: View<Editor>,
-    pending_update_matches: Option<Task<()>>,
-    confirm_on_update: Option<bool>,
-    width: Option<Length>,
-    max_height: Option<Length>,
-
-    /// Whether the `Picker` is rendered as a self-contained modal.
-    ///
-    /// Set this to `false` when rendering the `Picker` as part of a larger modal.
-    is_modal: bool,
-}
-
-pub trait PickerDelegate: Sized + 'static {
-    type ListItem: IntoElement;
-    fn match_count(&self) -> usize;
-    fn selected_index(&self) -> usize;
-    fn separators_after_indices(&self) -> Vec<usize> {
-        Vec::new()
-    }
-    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, cx: &mut ViewContext<Picker<Self>>);
-    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<Self::ListItem>;
-    fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
-        None
-    }
-    fn render_footer(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
-        None
-    }
-}
-
-impl<D: PickerDelegate> FocusableView for Picker<D> {
-    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.editor.focus_handle(cx)
-    }
-}
-
-impl<D: PickerDelegate> Picker<D> {
-    pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
-        let editor = cx.new_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();
-        let mut this = Self {
-            delegate,
-            editor,
-            scroll_handle: UniformListScrollHandle::new(),
-            pending_update_matches: None,
-            confirm_on_update: None,
-            width: None,
-            max_height: None,
-            is_modal: true,
-        };
-        this.update_matches("".to_string(), cx);
-        this
-    }
-
-    pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
-        self.width = Some(width.into());
-        self
-    }
-
-    pub fn max_height(mut self, max_height: impl Into<gpui::Length>) -> Self {
-        self.max_height = Some(max_height.into());
-        self
-    }
-
-    pub fn modal(mut self, modal: bool) -> Self {
-        self.is_modal = modal;
-        self
-    }
-
-    pub fn focus(&self, cx: &mut WindowContext) {
-        self.editor.update(cx, |editor, cx| editor.focus(cx));
-    }
-
-    pub 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);
-            cx.notify();
-        }
-    }
-
-    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);
-            cx.notify();
-        }
-    }
-
-    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);
-            cx.notify();
-        }
-    }
-
-    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);
-            cx.notify();
-        }
-    }
-
-    pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
-        let count = self.delegate.match_count();
-        let index = self.delegate.selected_index();
-        let new_index = if index + 1 == count { 0 } else { index + 1 };
-        self.delegate.set_selected_index(new_index, cx);
-        self.scroll_handle.scroll_to_item(new_index);
-        cx.notify();
-    }
-
-    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        self.delegate.dismissed(cx);
-        cx.emit(DismissEvent);
-    }
-
-    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        if self.pending_update_matches.is_some() {
-            self.confirm_on_update = Some(false)
-        } else {
-            self.delegate.confirm(false, cx);
-        }
-    }
-
-    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        if self.pending_update_matches.is_some() {
-            self.confirm_on_update = Some(true)
-        } else {
-            self.delegate.confirm(true, cx);
-        }
-    }
-
-    fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
-        cx.stop_propagation();
-        cx.prevent_default();
-        self.delegate.set_selected_index(ix, cx);
-        self.delegate.confirm(secondary, cx);
-    }
-
-    fn on_input_editor_event(
-        &mut self,
-        _: View<Editor>,
-        event: &editor::EditorEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            editor::EditorEvent::BufferEdited => {
-                let query = self.editor.read(cx).text(cx);
-                self.update_matches(query, cx);
-            }
-            editor::EditorEvent::Blurred => {
-                self.cancel(&menu::Cancel, cx);
-            }
-            _ => {}
-        }
-    }
-
-    pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
-        let query = self.editor.read(cx).text(cx);
-        self.update_matches(query, cx);
-    }
-
-    pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
-        let update = self.delegate.update_matches(query, cx);
-        self.matches_updated(cx);
-        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
-            update.await;
-            this.update(&mut cx, |this, cx| {
-                this.matches_updated(cx);
-            })
-            .ok();
-        }));
-    }
-
-    fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
-        let index = self.delegate.selected_index();
-        self.scroll_handle.scroll_to_item(index);
-        self.pending_update_matches = None;
-        if let Some(secondary) = self.confirm_on_update.take() {
-            self.delegate.confirm(secondary, cx);
-        }
-        cx.notify();
-    }
-
-    pub fn query(&self, cx: &AppContext) -> String {
-        self.editor.read(cx).text(cx)
-    }
-
-    pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| editor.set_text(query, cx));
-    }
-}
-
-impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
-impl<D: PickerDelegate> ModalView for Picker<D> {}
-
-impl<D: PickerDelegate> Render for Picker<D> {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let picker_editor = h_stack()
-            .overflow_hidden()
-            .flex_none()
-            .h_9()
-            .px_4()
-            .child(self.editor.clone());
-
-        div()
-            .key_context("Picker")
-            .size_full()
-            .when_some(self.width, |el, width| el.w(width))
-            .overflow_hidden()
-            // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
-            // as a part of a modal rather than the entire modal.
-            //
-            // We should revisit how the `Picker` is styled to make it more composable.
-            .when(self.is_modal, |this| this.elevation_3(cx))
-            .on_action(cx.listener(Self::select_next))
-            .on_action(cx.listener(Self::select_prev))
-            .on_action(cx.listener(Self::select_first))
-            .on_action(cx.listener(Self::select_last))
-            .on_action(cx.listener(Self::cancel))
-            .on_action(cx.listener(Self::confirm))
-            .on_action(cx.listener(Self::secondary_confirm))
-            .child(picker_editor)
-            .child(Divider::horizontal())
-            .when(self.delegate.match_count() > 0, |el| {
-                el.child(
-                    v_stack()
-                        .flex_grow()
-                        .py_2()
-                        .max_h(self.max_height.unwrap_or(rems(18.).into()))
-                        .overflow_hidden()
-                        .children(self.delegate.render_header(cx))
-                        .child(
-                            uniform_list(
-                                cx.view().clone(),
-                                "candidates",
-                                self.delegate.match_count(),
-                                {
-                                    let separators_after_indices = self.delegate.separators_after_indices();
-                                    let selected_index = self.delegate.selected_index();
-                                    move |picker, visible_range, cx| {
-                                        visible_range
-                                            .map(|ix| {
-                                                div()
-                                                    .on_mouse_down(
-                                                        MouseButton::Left,
-                                                        cx.listener(move |this, event: &MouseDownEvent, cx| {
-                                                            this.handle_click(
-                                                                ix,
-                                                                event.modifiers.command,
-                                                                cx,
-                                                            )
-                                                        }),
-                                                    )
-                                                    .children(picker.delegate.render_match(
-                                                        ix,
-                                                        ix == selected_index,
-                                                        cx,
-                                                    )).when(separators_after_indices.contains(&ix), |picker| picker.child(ListSeparator))
-                                            })
-                                            .collect()
-                                    }
-                                },
-                            )
-                            .track_scroll(self.scroll_handle.clone())
-                        )
-
-                )
-            })
-            .when(self.delegate.match_count() == 0, |el| {
-                el.child(
-                    v_stack().flex_grow().py_2().child(
-                        ListItem::new("empty_state")
-                            .inset(true)
-                            .spacing(ListItemSpacing::Sparse)
-                            .disabled(true)
-                            .child(Label::new("No matches").color(Color::Muted)),
-                    ),
-                )
-            })
-            .children(self.delegate.render_footer(cx))
-    }
-}

crates/project_symbols/Cargo.toml 🔗

@@ -12,7 +12,7 @@ doctest = false
 editor = { package = "editor2", path = "../editor2" }
 fuzzy = {package = "fuzzy2",  path = "../fuzzy2" }
 gpui = {package = "gpui2",  path = "../gpui2" }
-picker = {package = "picker2",  path = "../picker2" }
+picker = {path = "../picker" }
 project = { package = "project2", path = "../project2" }
 text = {package = "text2",  path = "../text2" }
 settings = {package = "settings2",  path = "../settings2" }

crates/recent_projects/Cargo.toml 🔗

@@ -13,7 +13,7 @@ editor = { package = "editor2", path = "../editor2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
 language = { package = "language2", path = "../language2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 settings = { package = "settings2", path = "../settings2" }
 text = { package = "text2", path = "../text2" }
 util = { path = "../util"}

crates/storybook2/Cargo.toml 🔗

@@ -34,7 +34,7 @@ theme2 = { path = "../theme2" }
 menu = { package = "menu2", path = "../menu2" }
 ui = { package = "ui2", path = "../ui2", features = ["stories"] }
 util = { path = "../util" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 
 [dev-dependencies]
 gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }

crates/theme_selector/Cargo.toml 🔗

@@ -15,7 +15,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
 fs = { package = "fs2", path = "../fs2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 settings = { package = "settings2", path = "../settings2" }
 theme = { package = "theme2", path = "../theme2" }
 ui = { package = "ui2", path = "../ui2" }

crates/vcs_menu/Cargo.toml 🔗

@@ -9,7 +9,7 @@ publish = false
 fuzzy = {package = "fuzzy2", path = "../fuzzy2"}
 fs = {package = "fs2", path = "../fs2"}
 gpui = {package = "gpui2", path = "../gpui2"}
-picker = {package = "picker2", path = "../picker2"}
+picker = {path = "../picker"}
 util = {path = "../util"}
 ui = {package = "ui2", path = "../ui2"}
 workspace = {package = "workspace2", path = "../workspace2"}

crates/welcome/Cargo.toml 🔗

@@ -24,7 +24,7 @@ settings = { package = "settings2", path = "../settings2" }
 theme = { package = "theme2", path = "../theme2" }
 theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }
-picker = { package = "picker2", path = "../picker2" }
+picker = { path = "../picker" }
 workspace = { package = "workspace2", path = "../workspace2" }
 vim = { path = "../vim" }
 

crates/zed/Cargo.toml 🔗

@@ -34,7 +34,7 @@ copilot_button = { path = "../copilot_button" }
 diagnostics = { path = "../diagnostics" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
-feedback = { package="feedback2", path = "../feedback2" }
+feedback = { path = "../feedback" }
 file_finder = { path = "../file_finder" }
 search = { path = "../search" }
 fs = { package = "fs2", path = "../fs2" }

crates/zed/src/app_menus.rs 🔗

@@ -150,7 +150,7 @@ pub fn app_menus() -> Vec<Menu<'static>> {
                 MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
                 MenuItem::action("Show Welcome", workspace::Welcome),
                 MenuItem::separator(),
-                // todo!(): Needs `feedback2` crate.
+                // todo!(): Needs `feedback` crate.
                 // MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback),
                 // MenuItem::action(
                 //     "Copy System Specs Into Clipboard",