init feedback2

Nate Butler , Joseph T. Lyons , and Conrad Irwin created

Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com>
Co-Authored-By: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

Cargo.lock                                     |  32 +
crates/feedback2/Cargo.toml                    |  42 +
crates/feedback2/src/deploy_feedback_button.rs |  91 ++++
crates/feedback2/src/feedback2.rs              |  62 ++
crates/feedback2/src/feedback_editor.rs        | 442 ++++++++++++++++++++
crates/feedback2/src/feedback_info_text.rs     |  94 ++++
crates/feedback2/src/submit_feedback_button.rs | 108 ++++
crates/feedback2/src/system_specs.rs           |  77 +++
crates/workspace2/src/workspace2.rs            |  21 
crates/zed2/Cargo.toml                         |   2 
10 files changed, 970 insertions(+), 1 deletion(-)

Detailed changes

Cargo.lock 🔗

@@ -3147,6 +3147,37 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "feedback2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "human_bytes",
+ "isahc",
+ "language2",
+ "lazy_static",
+ "log",
+ "postage",
+ "project2",
+ "regex",
+ "search2",
+ "serde",
+ "serde_derive",
+ "settings2",
+ "smallvec",
+ "sysinfo",
+ "theme2",
+ "tree-sitter-markdown",
+ "ui2",
+ "urlencoding",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "file-per-thread-logger"
 version = "0.1.6"
@@ -11741,6 +11772,7 @@ dependencies = [
  "editor2",
  "env_logger 0.9.3",
  "feature_flags2",
+ "feedback2",
  "file_finder2",
  "fs2",
  "fsevent",

crates/feedback2/Cargo.toml 🔗

@@ -0,0 +1,42 @@
+[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" }
+editor = { package = "editor2", path = "../editor2" }
+language = { package = "language2", path = "../language2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+project = { package = "project2", path = "../project2" }
+regex.workspace = true
+search = { package = "search2", path = "../search2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2"}
+ui = { package = "ui2", path = "../ui2" }
+
+log.workspace = true
+futures.workspace = true
+anyhow.workspace = true
+smallvec.workspace = true
+human_bytes = "0.4.1"
+isahc.workspace = true
+lazy_static.workspace = true
+postage.workspace = true
+serde.workspace = true
+serde_derive.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 🔗

@@ -0,0 +1,91 @@
+// use gpui::{
+//     elements::*,
+//     platform::{CursorStyle, MouseButton},
+//     Entity, View, ViewContext, WeakViewHandle,
+// };
+// use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+// use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
+
+// pub struct DeployFeedbackButton {
+//     active: bool,
+//     workspace: WeakViewHandle<Workspace>,
+// }
+
+// impl Entity for DeployFeedbackButton {
+//     type Event = ();
+// }
+
+// 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))
+//                         }
+//                     }
+//                 })
+//                 .with_tooltip::<Self>(
+//                     0,
+//                     "Send Feedback",
+//                     Some(Box::new(GiveFeedback)),
+//                     theme.tooltip.clone(),
+//                     cx,
+//                 ),
+//             )
+//             .into_any()
+//     }
+// }
+
+// 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();
+//     }
+// }

crates/feedback2/src/feedback2.rs 🔗

@@ -0,0 +1,62 @@
+pub mod deploy_feedback_button;
+pub mod feedback_editor;
+pub mod feedback_info_text;
+pub mod submit_feedback_button;
+
+mod system_specs;
+use gpui::{actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
+use system_specs::SystemSpecs;
+use workspace::Workspace;
+
+// actions!(
+//     zed,
+//     [
+//         CopySystemSpecsIntoClipboard,
+//         FileBugReport,
+//         RequestFeature,
+//         OpenZedCommunityRepo
+//     ]
+// );
+
+// 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);
+// }

crates/feedback2/src/feedback_editor.rs 🔗

@@ -0,0 +1,442 @@
+// 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/feedback2/src/feedback_info_text.rs 🔗

@@ -0,0 +1,94 @@
+// 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/feedback2/src/submit_feedback_button.rs 🔗

@@ -0,0 +1,108 @@
+// 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/feedback2/src/system_specs.rs 🔗

@@ -0,0 +1,77 @@
+// use client::ZED_APP_VERSION;
+// use gpui::{platform::AppVersion, 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>,
+//     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 platform = cx.platform();
+//         let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok());
+//         let release_channel = cx.global::<ReleaseChannel>().dev_name();
+//         let os_name = platform.os_name();
+//         let system = System::new_all();
+//         let memory = system.total_memory();
+//         let architecture = env::consts::ARCH;
+//         let os_version = platform
+//             .os_version()
+//             .ok()
+//             .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}")
+//     }
+// }
+
+// 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/workspace2/src/workspace2.rs 🔗

@@ -65,6 +65,7 @@ use std::{
 use theme2::{ActiveTheme, ThemeSettings};
 pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
+use ui::{h_stack, v_stack, StyledExt};
 use util::ResultExt;
 use uuid::Uuid;
 pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
@@ -3722,6 +3723,26 @@ impl Render for Workspace {
             .text_color(cx.theme().colors().text)
             .bg(cx.theme().colors().background)
             .children(self.titlebar_item.clone())
+            .child(
+                div()
+                    .absolute()
+                    .ml_1_4()
+                    .mt_20()
+                    .elevation_3(cx)
+                    .z_index(999)
+                    .w_1_2()
+                    .h_2_3()
+                    .child(
+                        v_stack().w_full().child(h_stack().child("header")),
+                        // Header
+                        // - has some info, maybe some links
+                        // Body
+                        // - Markdown Editor
+                        // - Email address
+                        // Footer
+                        // - CTA buttons (Send, Cancel)
+                    ),
+            )
             .child(
                 div()
                     .id("workspace")

crates/zed2/Cargo.toml 🔗

@@ -34,7 +34,7 @@ copilot = { package = "copilot2", path = "../copilot2" }
 diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
-# feedback = { path = "../feedback" }
+feedback = { package="feedback2", path = "../feedback2" }
 file_finder = { package="file_finder2", path = "../file_finder2" }
 search = { package = "search2", path = "../search2" }
 fs = { package = "fs2", path = "../fs2" }