WIP

Joseph T. Lyons created

Change summary

Cargo.lock                                     |   1 
crates/editor2/src/editor.rs                   |  11 
crates/feedback2/Cargo.toml                    |   1 
crates/feedback2/src/deploy_feedback_button.rs |   5 
crates/feedback2/src/feedback2.rs              |   5 
crates/feedback2/src/feedback_editor.rs        | 485 --------------------
crates/feedback2/src/feedback_info_text.rs     | 105 ----
crates/feedback2/src/feedback_modal.rs         | 209 +++++---
crates/feedback2/src/submit_feedback_button.rs | 115 ----
crates/zed2/src/zed2.rs                        |   4 
10 files changed, 146 insertions(+), 795 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3186,6 +3186,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "client2",
+ "db2",
  "editor2",
  "futures 0.3.28",
  "gpui2",

crates/editor2/src/editor.rs 🔗

@@ -8176,6 +8176,17 @@ impl Editor {
         self.buffer.read(cx).read(cx).text()
     }
 
+    pub fn text_option(&self, cx: &AppContext) -> Option<String> {
+        let text = self.buffer.read(cx).read(cx).text();
+        let text = text.trim();
+
+        if text.is_empty() {
+            return None;
+        }
+
+        Some(text.to_string())
+    }
+
     pub fn set_text(&mut self, text: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
             this.buffer

crates/feedback2/Cargo.toml 🔗

@@ -12,6 +12,7 @@ test-support = []
 
 [dependencies]
 client = { package = "client2", path = "../client2" }
+db = { package = "db2", path = "../db2" }
 editor = { package = "editor2", path = "../editor2" }
 language = { package = "language2", path = "../language2" }
 gpui = { package = "gpui2", path = "../gpui2" }

crates/feedback2/src/deploy_feedback_button.rs 🔗

@@ -2,17 +2,15 @@ use gpui::{AnyElement, Render, ViewContext, WeakView};
 use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
-use crate::{feedback_editor::GiveFeedback, feedback_modal::FeedbackModal};
+use crate::{feedback_modal::FeedbackModal, GiveFeedback};
 
 pub struct DeployFeedbackButton {
-    active: bool,
     workspace: WeakView<Workspace>,
 }
 
 impl DeployFeedbackButton {
     pub fn new(workspace: &Workspace) -> Self {
         DeployFeedbackButton {
-            active: false,
             workspace: workspace.weak_handle(),
         }
     }
@@ -48,6 +46,5 @@ impl StatusItemView for DeployFeedbackButton {
         _item: Option<&dyn ItemHandle>,
         _cx: &mut ViewContext<Self>,
     ) {
-        // no-op
     }
 }

crates/feedback2/src/feedback2.rs 🔗

@@ -3,10 +3,9 @@ use system_specs::SystemSpecs;
 use workspace::Workspace;
 
 pub mod deploy_feedback_button;
-pub mod feedback_editor;
-pub mod feedback_info_text;
 pub mod feedback_modal;
-pub mod submit_feedback_button;
+
+actions!(GiveFeedback, SubmitFeedback);
 
 mod system_specs;
 

crates/feedback2/src/feedback_editor.rs 🔗

@@ -1,485 +0,0 @@
-use crate::system_specs::SystemSpecs;
-use anyhow::bail;
-use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
-use editor::{Anchor, Editor, EditorEvent};
-use futures::AsyncReadExt;
-use gpui::{
-    actions, serde_json, AnyElement, AnyView, AppContext, Div, EntityId, EventEmitter,
-    FocusableView, Model, PromptLevel, Task, View, ViewContext, WindowContext,
-};
-use isahc::Request;
-use language::{Buffer, Event};
-use project::{search::SearchQuery, Project};
-use regex::Regex;
-use serde::Serialize;
-use std::{
-    any::TypeId,
-    ops::{Range, RangeInclusive},
-    sync::Arc,
-};
-use ui::{prelude::*, Icon, IconElement, Label};
-use util::ResultExt;
-use workspace::{
-    item::{Item, ItemEvent, ItemHandle},
-    searchable::{SearchEvent, 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!(GiveFeedback, SubmitFeedback);
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(|workspace: &mut Workspace, _| {
-        workspace.register_action(|workspace, _: &GiveFeedback, cx| {
-            FeedbackEditor::deploy(workspace, cx);
-        });
-    })
-    .detach();
-}
-
-#[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: View<Editor>,
-    project: Model<Project>,
-    pub allow_submission: bool,
-}
-
-impl EventEmitter<Event> for FeedbackEditor {}
-impl EventEmitter<EditorEvent> for FeedbackEditor {}
-
-impl FeedbackEditor {
-    fn new(
-        system_specs: SystemSpecs,
-        project: Model<Project>,
-        buffer: Model<Buffer>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let editor = cx.build_view(|cx| {
-            let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
-            editor.set_vertical_scroll_margin(5, cx);
-            editor
-        });
-
-        cx.subscribe(
-            &editor,
-            |&mut _, _, e: &EditorEvent, cx: &mut ViewContext<_>| 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 {
-            let prompt = cx.prompt(PromptLevel::Critical, &error, &["OK"]);
-            cx.spawn(|_, _cx| async move {
-                prompt.await.ok();
-            })
-            .detach();
-            return Task::ready(Ok(()));
-        }
-
-        let 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.await.ok();
-
-            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(Event::Closed))
-                            .log_err();
-                    }
-
-                    Err(error) => {
-                        log::error!("{}", error);
-                        this.update(&mut cx, |feedback_editor, cx| {
-                            let prompt = cx.prompt(
-                                PromptLevel::Critical,
-                                FEEDBACK_SUBMISSION_ERROR_TEXT,
-                                &["OK"],
-                            );
-                            cx.spawn(|_, _cx| async move {
-                                prompt.await.ok();
-                            })
-                            .detach();
-                            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.build_view(|cx| {
-                            FeedbackEditor::new(system_specs, project, buffer, cx)
-                        });
-                        workspace.add_item(Box::new(feedback_editor), cx);
-                    })
-                })?
-                .await
-        })
-        .detach_and_log_err(cx);
-    }
-}
-
-// TODO
-impl Render for FeedbackEditor {
-    type Element = Div;
-
-    fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
-        div().size_full().child(self.editor.clone())
-    }
-}
-
-impl EventEmitter<ItemEvent> for FeedbackEditor {}
-
-impl FocusableView for FeedbackEditor {
-    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-        self.editor.focus_handle(cx)
-    }
-}
-
-impl Item for FeedbackEditor {
-    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
-        Some("Send Feedback".into())
-    }
-
-    fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
-        h_stack()
-            .gap_1()
-            .child(IconElement::new(Icon::Envelope).color(Color::Accent))
-            .child(Label::new("Send Feedback".to_string()))
-            .into_any_element()
-    }
-
-    fn for_each_project_item(
-        &self,
-        cx: &AppContext,
-        f: &mut dyn FnMut(EntityId, &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,
-        _project: Model<Project>,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<anyhow::Result<()>> {
-        self.submit(cx)
-    }
-
-    fn save_as(
-        &mut self,
-        _: Model<Project>,
-        _: std::path::PathBuf,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<anyhow::Result<()>> {
-        self.submit(cx)
-    }
-
-    fn reload(&mut self, _: Model<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<View<Self>>
-    where
-        Self: Sized,
-    {
-        let buffer = self
-            .editor
-            .read(cx)
-            .buffer()
-            .read(cx)
-            .as_singleton()
-            .expect("Feedback buffer is only ever singleton");
-
-        Some(cx.build_view(|cx| {
-            Self::new(
-                self.system_specs.clone(),
-                self.project.clone(),
-                buffer.clone(),
-                cx,
-            )
-        }))
-    }
-
-    fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
-        Some(Box::new(handle.clone()))
-    }
-
-    fn act_as_type<'a>(
-        &'a self,
-        type_id: TypeId,
-        self_handle: &'a View<Self>,
-        cx: &'a AppContext,
-    ) -> Option<AnyView> {
-        if type_id == TypeId::of::<Self>() {
-            Some(self_handle.to_any())
-        } else if type_id == TypeId::of::<Editor>() {
-            Some(self.editor.to_any())
-        } else {
-            None
-        }
-    }
-
-    fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
-
-    fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
-
-    fn navigate(&mut self, _: Box<dyn std::any::Any>, _: &mut ViewContext<Self>) -> bool {
-        false
-    }
-
-    fn tab_description(&self, _: usize, _: &AppContext) -> Option<ui::prelude::SharedString> {
-        None
-    }
-
-    fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
-    fn is_dirty(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn has_conflict(&self, _: &AppContext) -> bool {
-        false
-    }
-
-    fn breadcrumb_location(&self) -> workspace::ToolbarItemLocation {
-        workspace::ToolbarItemLocation::Hidden
-    }
-
-    fn breadcrumbs(
-        &self,
-        _theme: &theme::Theme,
-        _cx: &AppContext,
-    ) -> Option<Vec<workspace::item::BreadcrumbText>> {
-        None
-    }
-
-    fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
-
-    fn serialized_item_kind() -> Option<&'static str> {
-        Some("feedback")
-    }
-
-    fn deserialize(
-        _project: gpui::Model<Project>,
-        _workspace: gpui::WeakView<Workspace>,
-        _workspace_id: workspace::WorkspaceId,
-        _item_id: workspace::ItemId,
-        _cx: &mut ViewContext<workspace::Pane>,
-    ) -> Task<anyhow::Result<View<Self>>> {
-        unimplemented!(
-            "deserialize() must be implemented if serialized_item_kind() returns Some(_)"
-        )
-    }
-
-    fn show_toolbar(&self) -> bool {
-        true
-    }
-
-    fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<gpui::Point<gpui::Pixels>> {
-        None
-    }
-}
-
-impl EventEmitter<SearchEvent> for FeedbackEditor {}
-
-impl SearchableItem for FeedbackEditor {
-    type Match = Range<Anchor>;
-
-    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/feedback2/src/feedback_info_text.rs 🔗

@@ -1,105 +0,0 @@
-use gpui::{Div, EventEmitter, View, ViewContext};
-use ui::{prelude::*, Label};
-use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
-
-use crate::feedback_editor::FeedbackEditor;
-
-pub struct FeedbackInfoText {
-    active_item: Option<View<FeedbackEditor>>,
-}
-
-impl FeedbackInfoText {
-    pub fn new() -> Self {
-        Self {
-            active_item: Default::default(),
-        }
-    }
-}
-
-// TODO
-impl Render for FeedbackInfoText {
-    type Element = Div;
-
-    fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
-        // TODO - get this into the toolbar area like before - ensure things work the same when horizontally shrinking app
-        div()
-            .size_full()
-            .child(Label::new("Share your feedback. Include your email for replies. For issues and discussions, visit the ").color(Color::Muted))
-            .child(Label::new("community repo").color(Color::Muted)) // TODO - this needs to be a link
-            .child(Label::new(".").color(Color::Muted))
-    }
-}
-
-// TODO - delete
-// 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 EventEmitter<ToolbarItemEvent> for FeedbackInfoText {}
-
-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>())
-        {
-            dbg!("Editor");
-            self.active_item = Some(feedback_editor);
-            ToolbarItemLocation::PrimaryLeft
-        } else {
-            dbg!("no editor");
-            self.active_item = None;
-            ToolbarItemLocation::Hidden
-        }
-    }
-}

crates/feedback2/src/feedback_modal.rs 🔗

@@ -1,29 +1,50 @@
-use std::ops::RangeInclusive;
+use std::{ops::RangeInclusive, sync::Arc};
 
+use anyhow::bail;
+use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorEvent};
+use futures::AsyncReadExt;
 use gpui::{
-    div, red, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
-    Render, View, ViewContext,
+    div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, Model, PromptLevel, Render, Task, View, ViewContext,
 };
+use isahc::Request;
 use language::Buffer;
 use project::Project;
+use regex::Regex;
+use serde_derive::Serialize;
 use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip};
 use util::ResultExt;
-use workspace::{item::Item, Workspace};
+use workspace::Workspace;
 
-use crate::{feedback_editor::GiveFeedback, system_specs::SystemSpecs, OpenZedCommunityRepo};
+use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
 
+const DATABASE_KEY_NAME: &str = "email_address";
+const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
 const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
 const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
     "Feedback failed to submit, see error log for details.";
 
+#[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,
+}
+
 pub struct FeedbackModal {
     system_specs: SystemSpecs,
     feedback_editor: View<Editor>,
     email_address_editor: View<Editor>,
     project: Model<Project>,
-    pub allow_submission: bool,
     character_count: usize,
+    allow_submission: bool,
+    pub pending_submission: bool,
 }
 
 impl FocusableView for FeedbackModal {
@@ -75,8 +96,14 @@ impl FeedbackModal {
         let email_address_editor = cx.build_view(|cx| {
             let mut editor = Editor::single_line(cx);
             editor.set_placeholder_text("Email address (optional)", cx);
+
+            if let Ok(Some(email_address)) = KEY_VALUE_STORE.read_kvp(DATABASE_KEY_NAME) {
+                editor.set_text(email_address, cx)
+            }
+
             editor
         });
+
         let feedback_editor = cx.build_view(|cx| {
             let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
             editor.set_vertical_scroll_margin(5, cx);
@@ -107,92 +134,116 @@ impl FeedbackModal {
             feedback_editor,
             email_address_editor,
             project,
-            allow_submission: true,
+            allow_submission: false,
+            pending_submission: false,
             character_count: 0,
         }
     }
 
-    // fn release(&mut self, cx: &mut WindowContext) {
-    //     let scroll_position = self.prev_scroll_position.take();
-    //     self.active_editor.update(cx, |editor, cx| {
-    //         editor.highlight_rows(None);
-    //         if let Some(scroll_position) = scroll_position {
-    //             editor.set_scroll_position(scroll_position, cx);
-    //         }
-    //         cx.notify();
-    //     })
-    // }
-
-    // fn on_feedback_editor_event(
-    //     &mut self,
-    //     _: View<Editor>,
-    //     event: &editor::EditorEvent,
-    //     cx: &mut ViewContext<Self>,
-    // ) {
-    //     match event {
-    //         // todo!() this isn't working...
-    //         editor::EditorEvent::Blurred => cx.emit(DismissEvent),
-    //         editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
-    //         _ => {}
-    //     }
-    // }
-
-    // fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
-    //     if let Some(point) = self.point_from_query(cx) {
-    //         self.active_editor.update(cx, |active_editor, cx| {
-    //             let snapshot = active_editor.snapshot(cx).display_snapshot;
-    //             let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
-    //             let display_point = point.to_display_point(&snapshot);
-    //             let row = display_point.row();
-    //             active_editor.highlight_rows(Some(row..row + 1));
-    //             active_editor.request_autoscroll(Autoscroll::center(), cx);
-    //         });
-    //         cx.notify();
-    //     }
-    // }
+    pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
+        if !self.allow_submission {
+            return Task::ready(Ok(()));
+        }
+        let feedback_text = self.feedback_editor.read(cx).text(cx).trim().to_string();
+        let email = self.email_address_editor.read(cx).text_option(cx);
 
-    // fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
-    //     let line_editor = self.line_editor.read(cx).text(cx);
-    //     let mut components = line_editor
-    //         .splitn(2, FILE_ROW_COLUMN_DELIMITER)
-    //         .map(str::trim)
-    //         .fuse();
-    //     let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
-    //     let column = components.next().and_then(|col| col.parse::<u32>().ok());
-    //     Some(Point::new(
-    //         row.saturating_sub(1),
-    //         column.unwrap_or(0).saturating_sub(1),
-    //     ))
-    // }
+        if let Some(email) = email.clone() {
+            cx.spawn(|_, _| KEY_VALUE_STORE.write_kvp(DATABASE_KEY_NAME.to_string(), email.clone()))
+                .detach()
+        }
 
-    // fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-    //     cx.emit(DismissEvent);
-    // }
+        let 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.await.ok();
+            if answer == Some(0) {
+                this.update(&mut cx, |feedback_editor, cx| {
+                    feedback_editor.set_pending_submission(true, cx);
+                })
+                .log_err();
+                match FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await {
+                    Ok(_) => {}
+                    Err(error) => {
+                        log::error!("{}", error);
+                        this.update(&mut cx, |feedback_editor, cx| {
+                            let prompt = cx.prompt(
+                                PromptLevel::Critical,
+                                FEEDBACK_SUBMISSION_ERROR_TEXT,
+                                &["OK"],
+                            );
+                            cx.spawn(|_, _cx| async move {
+                                prompt.await.ok();
+                            })
+                            .detach();
+                            feedback_editor.set_pending_submission(false, cx);
+                        })
+                        .log_err();
+                    }
+                }
+            }
+        })
+        .detach();
+        Task::ready(Ok(()))
+    }
 
-    // fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-    //     if let Some(point) = self.point_from_query(cx) {
-    //         self.active_editor.update(cx, |editor, cx| {
-    //             let snapshot = editor.snapshot(cx).display_snapshot;
-    //             let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
-    //             editor.change_selections(Some(Autoscroll::center()), cx, |s| {
-    //                 s.select_ranges([point..point])
-    //             });
-    //             editor.focus(cx);
-    //             cx.notify();
-    //         });
-    //         self.prev_scroll_position.take();
-    //     }
+    fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext<Self>) {
+        self.pending_submission = pending_submission;
+        cx.notify();
+    }
 
-    //     cx.emit(DismissEvent);
-    // }
+    async fn submit_feedback(
+        feedback_text: &str,
+        email: Option<String>,
+        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 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 Render for FeedbackModal {
     type Element = Div;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let character_count_error = (self.character_count < *FEEDBACK_CHAR_LIMIT.start())
-            || (self.character_count > *FEEDBACK_CHAR_LIMIT.end());
+        let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
+            Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
+            None => true,
+        };
+
+        self.allow_submission = FEEDBACK_CHAR_LIMIT.contains(&self.character_count)
+            && valid_email_address
+            && !self.pending_submission;
 
         let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
         // let open_community_issues =
@@ -268,7 +319,7 @@ impl Render for FeedbackModal {
                                         cx,
                                     )
                                 })
-                                .when(character_count_error, |this| this.disabled(true)),
+                                .when(!self.allow_submission, |this| this.disabled(true)),
                         ),
                     )
 

crates/feedback2/src/submit_feedback_button.rs 🔗

@@ -1,115 +0,0 @@
-use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
-use anyhow::Result;
-use gpui::{AppContext, Div, EventEmitter, Render, Task, View, ViewContext};
-use ui::prelude::*;
-use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
-
-pub fn init(cx: &mut AppContext) {
-    // cx.add_action(SubmitFeedbackButton::submit);
-}
-
-pub struct SubmitFeedbackButton {
-    pub(crate) active_item: Option<View<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
-        }
-    }
-}
-
-// TODO
-impl Render for SubmitFeedbackButton {
-    type Element = Div;
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let allow_submission = self
-            .active_item
-            .as_ref()
-            .map_or(true, |i| i.read(cx).allow_submission);
-
-        div()
-    }
-}
-
-// TODO - delete
-// impl View for 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 EventEmitter<ToolbarItemEvent> for SubmitFeedbackButton {}
-
-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
-        } else {
-            self.active_item = None;
-            ToolbarItemLocation::Hidden
-        }
-    }
-}

crates/zed2/src/zed2.rs 🔗

@@ -10,7 +10,6 @@ pub use assets::*;
 use breadcrumbs::Breadcrumbs;
 use collections::VecDeque;
 use editor::{Editor, MultiBuffer};
-use feedback::submit_feedback_button::SubmitFeedbackButton;
 use gpui::{
     actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions,
     ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions,
@@ -111,9 +110,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                             //     toolbar.add_item(diagnostic_editor_controls, cx);
                             //     let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
                             //     toolbar.add_item(project_search_bar, cx);
-                            let submit_feedback_button =
-                                cx.build_view(|_| SubmitFeedbackButton::new());
-                            toolbar.add_item(submit_feedback_button, cx);
                             //     let lsp_log_item =
                             //         cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
                             //     toolbar.add_item(lsp_log_item, cx);