WIP

Joseph Lyons created

Change summary

Cargo.lock                             |   3 
crates/feedback/Cargo.toml             |   3 
crates/feedback/src/feedback.rs        |   7 
crates/feedback/src/feedback_editor.rs | 183 +++++++++++++++++++--------
crates/zed/src/zed.rs                  |   2 
5 files changed, 137 insertions(+), 61 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2033,13 +2033,16 @@ dependencies = [
  "gpui",
  "human_bytes",
  "isahc",
+ "language",
  "lazy_static",
+ "postage",
  "project",
  "serde",
  "settings",
  "smallvec",
  "sysinfo",
  "theme",
+ "tree-sitter-markdown",
  "urlencoding",
  "util",
  "workspace",

crates/feedback/Cargo.toml 🔗

@@ -13,17 +13,20 @@ test-support = []
 anyhow = "1.0.38"
 client = { path = "../client" }
 editor = { path = "../editor" }
+language = { path = "../language" }
 futures = "0.3"
 gpui = { path = "../gpui" }
 human_bytes = "0.4.1"
 isahc = "1.7"
 lazy_static = "1.4.0"
+postage = { version = "0.4", features = ["futures-traits"] }
 project = { path = "../project" }
 serde = { version = "1.0", features = ["derive", "rc"] }
 settings = { path = "../settings" }
 smallvec = { version = "1.6", features = ["union"] }
 sysinfo = "0.27.1"
 theme = { path = "../theme" }
+tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
 util = { path = "../util" }
 workspace = { path = "../workspace" }

crates/feedback/src/feedback.rs 🔗

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-pub mod feedback_popover;
+pub mod feedback_editor;
 mod system_specs;
 use gpui::{actions, impl_actions, ClipboardItem, ViewContext};
 use serde::Deserialize;
@@ -21,7 +21,7 @@ actions!(
 );
 
 pub fn init(cx: &mut gpui::MutableAppContext) {
-    feedback_popover::init(cx);
+    feedback_editor::init(cx);
 
     cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
 
@@ -59,7 +59,4 @@ pub fn init(cx: &mut gpui::MutableAppContext) {
             });
         },
     );
-
-    // TODO FEEDBACK: Should I put Give Feedback action here?
-    // TODO FEEDBACK: Disble buffer search?
 }

crates/feedback/src/feedback_popover.rs → crates/feedback/src/feedback_editor.rs 🔗

@@ -2,15 +2,18 @@ use std::{ops::Range, sync::Arc};
 
 use anyhow::bail;
 use client::{Client, ZED_SECRET_CLIENT_TOKEN};
-use editor::{Editor, MultiBuffer};
+use editor::Editor;
 use futures::AsyncReadExt;
 use gpui::{
     actions,
     elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text},
-    serde_json, CursorStyle, Element, ElementBox, Entity, ModelHandle, MouseButton,
-    MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    serde_json, AnyViewHandle, CursorStyle, Element, ElementBox, Entity, ModelHandle, MouseButton,
+    MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
+    WeakViewHandle,
 };
 use isahc::Request;
+use language::{Language, LanguageConfig};
+use postage::prelude::Stream;
 
 use lazy_static::lazy_static;
 use project::{Project, ProjectEntryId, ProjectPath};
@@ -24,14 +27,13 @@ use workspace::{
 
 use crate::system_specs::SystemSpecs;
 
-// TODO FEEDBACK: Rename this file to feedback editor?
-// TODO FEEDBACK: Where is the backend code for air table?
-
 lazy_static! {
     pub static ref ZED_SERVER_URL: String =
         std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string());
 }
 
+// TODO FEEDBACK: In the future, it would be nice to use this is some sort of live-rendering character counter thing
+// Currently, we are just checking on submit that the the text exceeds the `start` value in this range
 const FEEDBACK_CHAR_COUNT_RANGE: Range<usize> = Range {
     start: 5,
     end: 1000,
@@ -40,7 +42,6 @@ const FEEDBACK_CHAR_COUNT_RANGE: Range<usize> = Range {
 actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
 
 pub fn init(cx: &mut MutableAppContext) {
-    // cx.add_action(FeedbackView::submit_feedback);
     cx.add_action(FeedbackEditor::deploy);
 }
 
@@ -147,14 +148,9 @@ impl StatusItemView for FeedbackButton {
         _: Option<&dyn ItemHandle>,
         _: &mut gpui::ViewContext<Self>,
     ) {
-        // N/A
     }
 }
 
-// impl Entity for FeedbackView {
-//     type Event = ();
-// }
-
 #[derive(Serialize)]
 struct FeedbackRequestBody<'a> {
     feedback_text: &'a str,
@@ -163,6 +159,7 @@ struct FeedbackRequestBody<'a> {
     token: &'a str,
 }
 
+#[derive(Clone)]
 struct FeedbackEditor {
     editor: ViewHandle<Editor>,
 }
@@ -173,15 +170,30 @@ impl FeedbackEditor {
         _: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        // TODO FEEDBACK: Get rid of this expect
+        // TODO FEEDBACK: This doesn't work like I expected it would
+        // let markdown_language = Arc::new(Language::new(
+        //     LanguageConfig::default(),
+        //     Some(tree_sitter_markdown::language()),
+        // ));
+
+        let markdown_language = project_handle
+            .read(cx)
+            .languages()
+            .get_language("Markdown")
+            .unwrap();
+
         let buffer = project_handle
-            .update(cx, |project, cx| project.create_buffer("", None, cx))
-            .expect("Could not open feedback window");
+            .update(cx, |project, cx| {
+                project.create_buffer("", Some(markdown_language), cx)
+            })
+            .expect("creating buffers on a local workspace always succeeds");
+
+        const FEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here in the form of Markdown. Save the tab to submit your feedback.";
 
         let editor = cx.add_view(|cx| {
             let mut editor = Editor::for_buffer(buffer, Some(project_handle.clone()), cx);
             editor.set_vertical_scroll_margin(5, cx);
-            editor.set_placeholder_text("Enter your feedback here, save to submit feedback", cx);
+            editor.set_placeholder_text(FEDBACK_PLACEHOLDER_TEXT, cx);
             editor
         });
 
@@ -189,7 +201,65 @@ impl FeedbackEditor {
         this
     }
 
-    fn submit_feedback(&mut self, cx: &mut ViewContext<'_, Self>) {
+    fn handle_save(
+        &mut self,
+        _: gpui::ModelHandle<Project>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<()>> {
+        // TODO FEEDBACK: These don't look right
+        let feedback_text_length = self.editor.read(cx).buffer().read(cx).len(cx);
+
+        if feedback_text_length <= FEEDBACK_CHAR_COUNT_RANGE.start {
+            cx.prompt(
+                PromptLevel::Critical,
+                &format!(
+                    "Feedback must be longer than {} characters",
+                    FEEDBACK_CHAR_COUNT_RANGE.start
+                ),
+                &["OK"],
+            );
+
+            return Task::ready(Ok(()));
+        }
+
+        let mut answer = cx.prompt(
+            PromptLevel::Warning,
+            "Ready to submit your feedback?",
+            &["Yes, Submit!", "No"],
+        );
+
+        let this = cx.handle();
+        cx.spawn(|_, mut cx| async move {
+            let answer = answer.recv().await;
+
+            if answer == Some(0) {
+                cx.update(|cx| {
+                    this.update(cx, |this, cx| match this.submit_feedback(cx) {
+                        // TODO FEEDBACK
+                        Ok(_) => {
+                            // Close file after feedback sent successfully
+                            // workspace
+                            //     .update(cx, |workspace, cx| {
+                            //         Pane::close_active_item(workspace, &Default::default(), cx)
+                            //             .unwrap()
+                            //     })
+                            //     .await
+                            //     .unwrap();
+                        }
+                        Err(error) => {
+                            cx.prompt(PromptLevel::Critical, &error.to_string(), &["OK"]);
+                            // Prompt that something failed (and to check the log for the exact error? and to try again?)
+                        }
+                    })
+                })
+            }
+        })
+        .detach();
+
+        Task::ready(Ok(()))
+    }
+
+    fn submit_feedback(&mut self, cx: &mut ViewContext<'_, Self>) -> anyhow::Result<()> {
         let feedback_text = self.editor.read(cx).text(cx);
         let zed_client = cx.global::<Arc<Client>>();
         let system_specs = SystemSpecs::new(cx);
@@ -198,13 +268,12 @@ impl FeedbackEditor {
         let metrics_id = zed_client.metrics_id();
         let http_client = zed_client.http_client();
 
-        cx.spawn(|_, _| {
-            async move {
-                // TODO FEEDBACK: Use or remove
-                // this.read_with(&async_cx, |this, cx| {
-                //     // Now we have a &self and a &AppContext
-                // });
+        // TODO FEEDBACK: how to get error out of the thread
+
+        let this = cx.handle();
 
+        cx.spawn(|_, async_cx| {
+            async move {
                 let request = FeedbackRequestBody {
                     feedback_text: &feedback_text,
                     metrics_id,
@@ -224,13 +293,14 @@ impl FeedbackEditor {
 
                 let response_status = response.status();
 
-                dbg!(response_status);
-
                 if !response_status.is_success() {
-                    // TODO FEEDBACK: Do some sort of error reporting here for if store fails
-                    bail!("Error")
+                    bail!("Feedback API failed with: {}", response_status)
                 }
 
+                this.read_with(&async_cx, |this, cx| -> anyhow::Result<()> {
+                    bail!("Error")
+                })?;
+
                 // TODO FEEDBACK: Use or remove
                 // Will need to handle error cases
                 // async_cx.update(|cx| {
@@ -246,6 +316,8 @@ impl FeedbackEditor {
             }
         })
         .detach();
+
+        Ok(())
     }
 }
 
@@ -258,25 +330,24 @@ impl FeedbackEditor {
         let feedback_editor = cx
             .add_view(|cx| FeedbackEditor::new(workspace.project().clone(), workspace_handle, cx));
         workspace.add_item(Box::new(feedback_editor), cx);
+        // }
     }
-    // }
 }
 
-// struct FeedbackView {
-//     editor: Editor,
-// }
-
 impl View for FeedbackEditor {
     fn ui_name() -> &'static str {
-        "Feedback"
+        "FeedbackEditor"
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        // let theme = cx.global::<Settings>().theme.clone();
-        // let submit_feedback_text_button_height = 20.0;
-
         ChildView::new(&self.editor, cx).boxed()
     }
+
+    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(&self.editor);
+        }
+    }
 }
 
 impl Entity for FeedbackEditor {
@@ -324,26 +395,19 @@ impl Item for FeedbackEditor {
 
     fn save(
         &mut self,
-        _: gpui::ModelHandle<Project>,
+        project_handle: gpui::ModelHandle<Project>,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        cx.prompt(
-            gpui::PromptLevel::Info,
-            &format!("You are trying to to submit this feedbac"),
-            &["OK"],
-        );
-
-        self.submit_feedback(cx);
-        Task::ready(Ok(()))
+        self.handle_save(project_handle, cx)
     }
 
     fn save_as(
         &mut self,
-        _: gpui::ModelHandle<Project>,
+        project_handle: gpui::ModelHandle<Project>,
         _: std::path::PathBuf,
-        _: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        unreachable!("save_as should not have been called");
+        self.handle_save(project_handle, cx)
     }
 
     fn reload(
@@ -351,7 +415,20 @@ impl Item for FeedbackEditor {
         _: gpui::ModelHandle<Project>,
         _: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>> {
-        unreachable!("should not have been called")
+        unreachable!("reload should not have been called")
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: workspace::WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        // TODO FEEDBACK: split is busted
+        // Some(self.clone())
+        None
     }
 
     fn serialized_item_kind() -> Option<&'static str> {
@@ -369,9 +446,5 @@ impl Item for FeedbackEditor {
     }
 }
 
-// TODO FEEDBACK: Add placeholder text
-// TODO FEEDBACK: act_as_type (max mentionedt this)
-// TODO FEEDBACK: focus
-// TODO FEEDBACK: markdown highlighting
-// TODO FEEDBACK: save prompts and accepting closes
-// TODO FEEDBACK: multiple tabs?
+// TODO FEEDBACK: search buffer?
+// TODO FEEDBACK: warnings

crates/zed/src/zed.rs 🔗

@@ -327,7 +327,7 @@ pub fn initialize_workspace(
     let activity_indicator =
         activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
-    let feedback_button = cx.add_view(|_| feedback::feedback_popover::FeedbackButton {});
+    let feedback_button = cx.add_view(|_| feedback::feedback_editor::FeedbackButton {});
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic_summary, cx);
         status_bar.add_left_item(activity_indicator, cx);