From f987a640fd691a3dbad83c27d2ac84252a6befb6 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 15:23:42 -0500 Subject: [PATCH 01/27] init feedback2 Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> Co-Authored-By: Conrad Irwin --- Cargo.lock | 32 ++ crates/feedback2/Cargo.toml | 42 ++ .../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 ++++ .../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(-) create mode 100644 crates/feedback2/Cargo.toml create mode 100644 crates/feedback2/src/deploy_feedback_button.rs create mode 100644 crates/feedback2/src/feedback2.rs create mode 100644 crates/feedback2/src/feedback_editor.rs create mode 100644 crates/feedback2/src/feedback_info_text.rs create mode 100644 crates/feedback2/src/submit_feedback_button.rs create mode 100644 crates/feedback2/src/system_specs.rs diff --git a/Cargo.lock b/Cargo.lock index 3110a9ff43..fcaa533f73 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/feedback2/Cargo.toml b/crates/feedback2/Cargo.toml new file mode 100644 index 0000000000..fbf033919d --- /dev/null +++ b/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"] } diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs new file mode 100644 index 0000000000..ba58ed13d3 --- /dev/null +++ b/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, +// } + +// 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) -> AnyElement { +// let active = self.active; +// let theme = theme::current(cx).clone(); +// Stack::new() +// .with_child( +// MouseEventHandler::new::(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::( +// 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) { +// if let Some(item) = item { +// if let Some(_) = item.downcast::() { +// self.active = true; +// cx.notify(); +// return; +// } +// } +// self.active = false; +// cx.notify(); +// } +// } diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs new file mode 100644 index 0000000000..5b26d60074 --- /dev/null +++ b/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| { +// 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| { +// 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| { +// 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); +// } diff --git a/crates/feedback2/src/feedback_editor.rs b/crates/feedback2/src/feedback_editor.rs new file mode 100644 index 0000000000..8f850f7293 --- /dev/null +++ b/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 = 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| { +// FeedbackEditor::deploy(workspace, cx); +// } +// }); +// } + +// #[derive(Serialize)] +// struct FeedbackRequestBody<'a> { +// feedback_text: &'a str, +// email: Option, +// metrics_id: Option>, +// installation_id: Option>, +// system_specs: SystemSpecs, +// is_staff: bool, +// token: &'a str, +// } + +// #[derive(Clone)] +// pub(crate) struct FeedbackEditor { +// system_specs: SystemSpecs, +// editor: ViewHandle, +// project: ModelHandle, +// pub allow_submission: bool, +// } + +// impl FeedbackEditor { +// fn new( +// system_specs: SystemSpecs, +// project: ModelHandle, +// buffer: ModelHandle, +// cx: &mut ViewContext, +// ) -> 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) -> Task> { +// 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::>().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.allow_submission = allow_submission; +// cx.notify(); +// } + +// async fn submit_feedback( +// feedback_text: &str, +// zed_client: Arc, +// 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) { +// 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) -> AnyElement { +// ChildView::new(&self.editor, cx).into_any() +// } + +// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { +// 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> { +// Some("Send Feedback".into()) +// } + +// fn tab_content( +// &self, +// _: Option, +// style: &theme::Tab, +// _: &AppContext, +// ) -> AnyElement { +// 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, +// cx: &mut ViewContext, +// ) -> Task> { +// self.submit(cx) +// } + +// fn save_as( +// &mut self, +// _: ModelHandle, +// _: std::path::PathBuf, +// cx: &mut ViewContext, +// ) -> Task> { +// self.submit(cx) +// } + +// fn reload( +// &mut self, +// _: ModelHandle, +// _: &mut ViewContext, +// ) -> Task> { +// Task::Ready(Some(Ok(()))) +// } + +// fn clone_on_split( +// &self, +// _workspace_id: workspace::WorkspaceId, +// cx: &mut ViewContext, +// ) -> Option +// 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) -> Option> { +// Some(Box::new(handle.clone())) +// } + +// fn act_as_type<'a>( +// &'a self, +// type_id: TypeId, +// self_handle: &'a ViewHandle, +// _: &'a AppContext, +// ) -> Option<&'a AnyViewHandle> { +// if type_id == TypeId::of::() { +// Some(self_handle) +// } else if type_id == TypeId::of::() { +// 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; + +// fn to_search_event( +// &mut self, +// event: &Self::Event, +// cx: &mut ViewContext, +// ) -> Option { +// self.editor +// .update(cx, |editor, cx| editor.to_search_event(event, cx)) +// } + +// fn clear_matches(&mut self, cx: &mut ViewContext) { +// self.editor +// .update(cx, |editor, cx| editor.clear_matches(cx)) +// } + +// fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { +// self.editor +// .update(cx, |editor, cx| editor.update_matches(matches, cx)) +// } + +// fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { +// self.editor +// .update(cx, |editor, cx| editor.query_suggestion(cx)) +// } + +// fn activate_match( +// &mut self, +// index: usize, +// matches: Vec, +// cx: &mut ViewContext, +// ) { +// self.editor +// .update(cx, |editor, cx| editor.activate_match(index, matches, cx)) +// } + +// fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { +// self.editor +// .update(cx, |e, cx| e.select_matches(matches, cx)) +// } +// fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext) { +// self.editor +// .update(cx, |e, cx| e.replace(matches, query, cx)); +// } +// fn find_matches( +// &mut self, +// query: Arc, +// cx: &mut ViewContext, +// ) -> Task> { +// self.editor +// .update(cx, |editor, cx| editor.find_matches(query, cx)) +// } + +// fn active_match_index( +// &mut self, +// matches: Vec, +// cx: &mut ViewContext, +// ) -> Option { +// self.editor +// .update(cx, |editor, cx| editor.active_match_index(matches, cx)) +// } +// } diff --git a/crates/feedback2/src/feedback_info_text.rs b/crates/feedback2/src/feedback_info_text.rs new file mode 100644 index 0000000000..96c76929ea --- /dev/null +++ b/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>, +// } + +// 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) -> AnyElement { +// 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::(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, +// ) -> workspace::ToolbarItemLocation { +// cx.notify(); +// if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) +// { +// self.active_item = Some(feedback_editor); +// ToolbarItemLocation::PrimaryLeft { +// flex: Some((1., false)), +// } +// } else { +// self.active_item = None; +// ToolbarItemLocation::Hidden +// } +// } +// } diff --git a/crates/feedback2/src/submit_feedback_button.rs b/crates/feedback2/src/submit_feedback_button.rs new file mode 100644 index 0000000000..4b6fc70920 --- /dev/null +++ b/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>, +// } + +// impl SubmitFeedbackButton { +// pub fn new() -> Self { +// Self { +// active_item: Default::default(), +// } +// } + +// pub fn submit( +// &mut self, +// _: &SubmitFeedback, +// cx: &mut ViewContext, +// ) -> Option>> { +// 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) -> AnyElement { +// 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::(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::( +// 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, +// ) -> workspace::ToolbarItemLocation { +// cx.notify(); +// if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) +// { +// self.active_item = Some(feedback_editor); +// ToolbarItemLocation::PrimaryRight { flex: None } +// } else { +// self.active_item = None; +// ToolbarItemLocation::Hidden +// } +// } +// } diff --git a/crates/feedback2/src/system_specs.rs b/crates/feedback2/src/system_specs.rs new file mode 100644 index 0000000000..b2541c2bab --- /dev/null +++ b/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, +// release_channel: &'static str, +// os_name: &'static str, +// os_version: Option, +// 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::().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::>() +// .join("\n"); + +// write!(f, "{system_specs}") +// } +// } + +// fn serialize_app_version(version: &Option, serializer: S) -> Result +// where +// S: serde::Serializer, +// { +// version.map(|v| v.to_string()).serialize(serializer) +// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index cbd3e4309c..a096deb97f 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/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") diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index bd2a8e5a2f..94e601d2f8 100644 --- a/crates/zed2/Cargo.toml +++ b/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" } From 5098fafa026d613ab9cb6386e892452c77f494eb Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 15:28:18 -0500 Subject: [PATCH 02/27] Add `v_stack` and `h_stack` to the UI prelude Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> Co-Authored-By: Conrad Irwin --- crates/ui2/src/prelude.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 6fd0262c67..38065b6275 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -8,5 +8,6 @@ pub use crate::clickable::*; pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; +pub use crate::{h_stack, v_stack}; pub use crate::{ButtonCommon, Color, StyledExt}; pub use theme::ActiveTheme; From 8b7be8f6149dba633a7f8870da0a902fb2a43c8c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 16:07:54 -0500 Subject: [PATCH 03/27] Get `feedback2` compiling Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> Co-Authored-By: Conrad Irwin --- .../feedback2/src/deploy_feedback_button.rs | 137 ++++++--------- crates/feedback2/src/feedback2.rs | 11 +- crates/feedback2/src/feedback_editor.rs | 31 ++-- crates/feedback2/src/feedback_modal.rs | 164 ++++++++++++++++++ .../feedback2/src/submit_feedback_button.rs | 118 +++++++------ crates/feedback2/src/system_specs.rs | 27 ++- crates/workspace2/src/workspace2.rs | 20 --- crates/zed2/src/zed2.rs | 11 +- 8 files changed, 313 insertions(+), 206 deletions(-) create mode 100644 crates/feedback2/src/feedback_modal.rs diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs index ba58ed13d3..800f8d70f8 100644 --- a/crates/feedback2/src/deploy_feedback_button.rs +++ b/crates/feedback2/src/deploy_feedback_button.rs @@ -1,91 +1,58 @@ -// use gpui::{ -// elements::*, -// platform::{CursorStyle, MouseButton}, -// Entity, View, ViewContext, WeakViewHandle, -// }; -// use workspace::{item::ItemHandle, StatusItemView, Workspace}; +use gpui::{Action, AnyElement, Render, ViewContext, WeakView}; +use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip}; +use workspace::{StatusItemView, Workspace}; -// use crate::feedback_editor::{FeedbackEditor, GiveFeedback}; +use crate::{feedback_editor::GiveFeedback, feedback_modal::FeedbackModal}; -// pub struct DeployFeedbackButton { -// active: bool, -// workspace: WeakViewHandle, -// } +pub struct DeployFeedbackButton { + active: bool, + workspace: WeakView, +} -// impl Entity for DeployFeedbackButton { -// type Event = (); -// } +impl DeployFeedbackButton { + pub fn new(workspace: &Workspace) -> Self { + DeployFeedbackButton { + active: false, + workspace: workspace.weak_handle(), + } + } +} -// impl DeployFeedbackButton { -// pub fn new(workspace: &Workspace) -> Self { -// DeployFeedbackButton { -// active: false, -// workspace: workspace.weak_handle(), -// } -// } -// } +impl Render for DeployFeedbackButton { + type Element = AnyElement; -// impl View for DeployFeedbackButton { -// fn ui_name() -> &'static str { -// "DeployFeedbackButton" -// } + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let is_open = self + .workspace + .upgrade() + .and_then(|workspace| { + workspace.update(cx, |workspace, cx| { + workspace.active_modal::(cx) + }) + }) + .is_some(); -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// let active = self.active; -// let theme = theme::current(cx).clone(); -// Stack::new() -// .with_child( -// MouseEventHandler::new::(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::( -// 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) { -// if let Some(item) = item { -// if let Some(_) = item.downcast::() { -// self.active = true; -// cx.notify(); -// return; -// } -// } -// self.active = false; -// cx.notify(); -// } -// } + IconButton::new("give-feedback", Icon::Envelope) + .style(ui::ButtonStyle::Subtle) + .selected(is_open) + .tooltip(|cx| Tooltip::text("Give Feedback", cx)) + .on_click(cx.listener(|this, _, cx| { + let Some(workspace) = this.workspace.upgrade() else { + return; + }; + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| FeedbackModal::new(cx)) + }) + })) + .into_any_element() + } +} +impl StatusItemView for DeployFeedbackButton { + fn set_active_pane_item( + &mut self, + _active_pane_item: Option<&dyn workspace::item::ItemHandle>, + _cx: &mut ViewContext, + ) { + // no-op + } +} diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index 5b26d60074..8bacc4255e 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/crates/feedback2/src/feedback2.rs @@ -1,12 +1,17 @@ +use gpui::AppContext; + pub mod deploy_feedback_button; pub mod feedback_editor; pub mod feedback_info_text; +pub mod feedback_modal; pub mod submit_feedback_button; mod system_specs; -use gpui::{actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext}; -use system_specs::SystemSpecs; -use workspace::Workspace; + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(feedback_modal::FeedbackModal::register) + .detach(); +} // actions!( // zed, diff --git a/crates/feedback2/src/feedback_editor.rs b/crates/feedback2/src/feedback_editor.rs index 8f850f7293..258b3553f8 100644 --- a/crates/feedback2/src/feedback_editor.rs +++ b/crates/feedback2/src/feedback_editor.rs @@ -3,38 +3,27 @@ // 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 gpui::{actions, serde_json, AppContext, Model, PromptLevel, Task, View, ViewContext}; // 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, -// }; +// use workspace::{searchable::SearchableItem, Workspace}; // const FEEDBACK_CHAR_LIMIT: RangeInclusive = 10..=5000; // const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = // "Feedback failed to submit, see error log for details."; -// actions!(feedback, [GiveFeedback, SubmitFeedback]); +use gpui::actions; + +actions!(GiveFeedback, SubmitFeedback); // pub fn init(cx: &mut AppContext) { // cx.add_action({ @@ -58,16 +47,16 @@ // #[derive(Clone)] // pub(crate) struct FeedbackEditor { // system_specs: SystemSpecs, -// editor: ViewHandle, -// project: ModelHandle, +// editor: View, +// project: Model, // pub allow_submission: bool, // } // impl FeedbackEditor { // fn new( // system_specs: SystemSpecs, -// project: ModelHandle, -// buffer: ModelHandle, +// project: Model, +// buffer: Model, // cx: &mut ViewContext, // ) -> Self { // let editor = cx.add_view(|cx| { @@ -135,7 +124,7 @@ // match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await { // Ok(_) => { -// this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed)) +// this.update(&mut cx, |_, cx| cx.emit(editor::EditorEvent::Closed)) // .log_err(); // } diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs new file mode 100644 index 0000000000..3335576028 --- /dev/null +++ b/crates/feedback2/src/feedback_modal.rs @@ -0,0 +1,164 @@ +use gpui::{ + div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, + ViewContext, +}; +use ui::prelude::*; +use workspace::Workspace; + +use crate::feedback_editor::GiveFeedback; + +pub struct FeedbackModal { + // editor: View, + tmp_focus_handle: FocusHandle, // TODO: should be editor.focus_handle(cx) +} + +impl FocusableView for FeedbackModal { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.tmp_focus_handle.clone() + } +} +impl EventEmitter for FeedbackModal {} + +impl FeedbackModal { + pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { + let _handle = cx.view().downgrade(); + workspace.register_action(move |workspace, _: &GiveFeedback, cx| { + workspace.toggle_modal(cx, move |cx| FeedbackModal::new(cx)); + }); + } + + pub fn new(cx: &mut ViewContext) -> Self { + Self { + tmp_focus_handle: cx.focus_handle(), + } + } + + // 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, + // event: &editor::EditorEvent, + // cx: &mut ViewContext, + // ) { + // 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) { + // 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(); + // } + // } + + // fn point_from_query(&self, cx: &ViewContext) -> Option { + // 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::().ok())?; + // let column = components.next().and_then(|col| col.parse::().ok()); + // Some(Point::new( + // row.saturating_sub(1), + // column.unwrap_or(0).saturating_sub(1), + // )) + // } + + // fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + // cx.emit(DismissEvent); + // } + + // fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // 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(); + // } + + // cx.emit(DismissEvent); + // } +} + +impl Render for FeedbackModal { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().elevation_3(cx).w_1_2().h_2_3().child( + v_stack() + .w_full() + .child(h_stack().child("header")) + .child("editor"), + // Header + // - has some info, maybe some links + // Body + // - Markdown Editor + // - Email address + // Footer + // - CTA buttons (Send, Cancel) + ) + + // div() + // .elevation_2(cx) + // .key_context( + // "FeedbackModal + // ", + // ) + // .on_action(cx.listener(Self::cancel)) + // .on_action(cx.listener(Self::confirm)) + // .w_96() + // .child( + // v_stack() + // .px_1() + // .pt_0p5() + // .gap_px() + // .child( + // v_stack() + // .py_0p5() + // .px_1() + // .child(div().px_1().py_0p5().child(self.line_editor.clone())), + // ) + // .child( + // div() + // .h_px() + // .w_full() + // .bg(cx.theme().colors().element_background), + // ) + // .child( + // h_stack() + // .justify_between() + // .px_2() + // .py_1() + // .child(Label::new(self.current_text.clone()).color(Color::Muted)), + // ), + // ) + } +} diff --git a/crates/feedback2/src/submit_feedback_button.rs b/crates/feedback2/src/submit_feedback_button.rs index 4b6fc70920..78a66619a2 100644 --- a/crates/feedback2/src/submit_feedback_button.rs +++ b/crates/feedback2/src/submit_feedback_button.rs @@ -1,10 +1,7 @@ -// use crate::feedback_editor::{FeedbackEditor, SubmitFeedback}; +// use crate::{feedback_editor::SubmitFeedback, feedback_modal::FeedbackModal}; // use anyhow::Result; -// use gpui::{ -// elements::{Label, MouseEventHandler}, -// platform::{CursorStyle, MouseButton}, -// AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle, -// }; +// use gpui::{AppContext, Render, Task, View, ViewContext}; +// use ui::IconButton; // use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; // pub fn init(cx: &mut AppContext) { @@ -12,7 +9,7 @@ // } // pub struct SubmitFeedbackButton { -// pub(crate) active_item: Option>, +// pub(crate) active_item: Option>, // } // impl SubmitFeedbackButton { @@ -35,60 +32,67 @@ // } // } -// impl Entity for SubmitFeedbackButton { -// type Event = (); -// } +// impl Render for SubmitFeedbackbutton { +// type Element; -// impl View for SubmitFeedbackButton { -// fn ui_name() -> &'static str { -// "SubmitFeedbackButton" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// 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::(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::( -// 0, -// "cmd-s", -// Some(Box::new(SubmitFeedback)), -// theme.tooltip.clone(), -// cx, -// ) -// .into_any() +// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { +// todo!(); +// // IconButton::new("give-feedback", Icon::Envelope) +// // .style(ui::ButtonStyle::Subtle) +// // .on_click(|_, cx| cx.dispatch_action(GiveFeedback)) // } // } +// // impl View for SubmitFeedbackButton { +// // fn ui_name() -> &'static str { +// // "SubmitFeedbackButton" +// // } + +// // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// // 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::(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::( +// // 0, +// // "cmd-s", +// // Some(Box::new(SubmitFeedback)), +// // theme.tooltip.clone(), +// // cx, +// // ) +// // .into_any() +// // } +// // } + // impl ToolbarItemView for SubmitFeedbackButton { // fn set_active_pane_item( // &mut self, diff --git a/crates/feedback2/src/system_specs.rs b/crates/feedback2/src/system_specs.rs index b2541c2bab..1a2c8775a6 100644 --- a/crates/feedback2/src/system_specs.rs +++ b/crates/feedback2/src/system_specs.rs @@ -1,17 +1,16 @@ -// use client::ZED_APP_VERSION; -// use gpui::{platform::AppVersion, AppContext}; +// // 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; -// 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, +// // todo!() +// // #[serde(serialize_with = "serialize_app_version")] +// // app_version: Option, // release_channel: &'static str, // os_name: &'static str, // os_version: Option, @@ -22,7 +21,7 @@ // 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(|| platform.app_version().ok()); // let release_channel = cx.global::().dev_name(); // let os_name = platform.os_name(); // let system = System::new_all(); @@ -34,7 +33,7 @@ // .map(|os_version| os_version.to_string()); // SystemSpecs { -// app_version, +// // app_version, // release_channel, // os_name, // os_version, @@ -69,9 +68,9 @@ // } // } -// fn serialize_app_version(version: &Option, serializer: S) -> Result -// where -// S: serde::Serializer, -// { -// version.map(|v| v.to_string()).serialize(serializer) -// } +// // fn serialize_app_version(version: &Option, serializer: S) -> Result +// // where +// // S: serde::Serializer, +// // { +// // version.map(|v| v.to_string()).serialize(serializer) +// // } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index a096deb97f..7d5db4d8d1 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3723,26 +3723,6 @@ 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") diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 07b75bf8d4..2fc3a46549 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -109,8 +109,8 @@ pub fn initialize_workspace(app_state: Arc, 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.add_view(|_| SubmitFeedbackButton::new()); + // let submit_feedback_button = + // cx.build_view(|_| SubmitFeedbackButton::new()); // toolbar.add_item(submit_feedback_button, cx); // let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); // toolbar.add_item(feedback_info_text, cx); @@ -144,15 +144,14 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // let active_buffer_language = // cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); // let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx)); - // let feedback_button = cx.add_view(|_| { - // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) - // }); + let feedback_button = cx + .build_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)); // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); - // status_bar.add_right_item(feedback_button, cx); + status_bar.add_right_item(feedback_button, cx); // status_bar.add_right_item(copilot, cx); // status_bar.add_right_item(active_buffer_language, cx); // status_bar.add_right_item(vim_mode_indicator, cx); From e228422f1444c15b6bb4f90f64c0946b807c4fd5 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 16:42:58 -0500 Subject: [PATCH 04/27] Update feedback modal UI Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- crates/feedback2/src/feedback_modal.rs | 64 ++++++++++++++++++++------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 3335576028..bc42c3ec3c 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -1,8 +1,8 @@ use gpui::{ - div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, + div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, ViewContext, }; -use ui::prelude::*; +use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; use workspace::Workspace; use crate::feedback_editor::GiveFeedback; @@ -112,19 +112,53 @@ impl Render for FeedbackModal { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div().elevation_3(cx).w_1_2().h_2_3().child( - v_stack() - .w_full() - .child(h_stack().child("header")) - .child("editor"), - // Header - // - has some info, maybe some links - // Body - // - Markdown Editor - // - Email address - // Footer - // - CTA buttons (Send, Cancel) - ) + v_stack() + .elevation_3(cx) + .min_w(rems(40.)) + .max_w(rems(96.)) + .h(rems(40.)) + .p_2() + .gap_2() + .child(h_stack().child(Label::new("Give Feedback").color(Color::Default))) + .child( + div() + .flex_1() + .bg(cx.theme().colors().editor_background) + .border() + .border_color(cx.theme().colors().border) + .child("editor"), + ) + .child( + h_stack() + .justify_end() + .gap_1() + .child( + Button::new("cancel_feedback", "Cancel") + .style(ButtonStyle::Subtle) + .color(Color::Muted), + ) + .child( + Button::new("send_feedback", "Send Feedback") + .color(Color::Accent) + .style(ButtonStyle::Filled) + .tooltip(|cx| { + Tooltip::with_meta( + "Submit feedback to the Zed team.", + None, + "Provide an email address if you want us to be able to reply.", + cx, + ) + }), + ), + ) + + // Header + // - has some info, maybe some links + // Body + // - Markdown Editor + // - Email address + // Footer + // - CTA buttons (Send, Cancel) // div() // .elevation_2(cx) From a75f1a4a954a909eeea4c712d77464e9f6cd696b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 30 Nov 2023 22:07:10 -0500 Subject: [PATCH 05/27] Close feedback modal when clicking cancel --- crates/feedback2/src/feedback_modal.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index bc42c3ec3c..90ddb23ab2 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -112,6 +112,8 @@ impl Render for FeedbackModal { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); + v_stack() .elevation_3(cx) .min_w(rems(40.)) @@ -135,7 +137,8 @@ impl Render for FeedbackModal { .child( Button::new("cancel_feedback", "Cancel") .style(ButtonStyle::Subtle) - .color(Color::Muted), + .color(Color::Muted) + .on_click(dismiss), ) .child( Button::new("send_feedback", "Send Feedback") From a80b145d0d26b14e5484a89f1e7f2cd980b757b3 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 5 Dec 2023 13:04:53 -0500 Subject: [PATCH 06/27] temp --- crates/feedback2/src/feedback_editor.rs | 20 ++++++++++---------- crates/feedback2/src/feedback_modal.rs | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/crates/feedback2/src/feedback_editor.rs b/crates/feedback2/src/feedback_editor.rs index 258b3553f8..396cd27a6a 100644 --- a/crates/feedback2/src/feedback_editor.rs +++ b/crates/feedback2/src/feedback_editor.rs @@ -33,16 +33,16 @@ actions!(GiveFeedback, SubmitFeedback); // }); // } -// #[derive(Serialize)] -// struct FeedbackRequestBody<'a> { -// feedback_text: &'a str, -// email: Option, -// metrics_id: Option>, -// installation_id: Option>, -// system_specs: SystemSpecs, -// is_staff: bool, -// token: &'a str, -// } +#[derive(Serialize)] +struct FeedbackRequestBody<'a> { + feedback_text: &'a str, + email: Option, + metrics_id: Option>, + installation_id: Option>, + system_specs: SystemSpecs, + is_staff: bool, + token: &'a str, +} // #[derive(Clone)] // pub(crate) struct FeedbackEditor { diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 90ddb23ab2..f04f40541f 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -1,3 +1,4 @@ +use editor::Editor; use gpui::{ div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, ViewContext, @@ -8,7 +9,7 @@ use workspace::Workspace; use crate::feedback_editor::GiveFeedback; pub struct FeedbackModal { - // editor: View, + editor: View, tmp_focus_handle: FocusHandle, // TODO: should be editor.focus_handle(cx) } @@ -28,7 +29,22 @@ impl FeedbackModal { } pub fn new(cx: &mut ViewContext) -> Self { + let line_editor = cx.build_view(|cx| Editor::single_line(cx)); + let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); + + // let editor = active_editor.read(cx); + // let cursor = editor.selections.last::(cx).head(); + // let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row; + // let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx)); + + // let current_text = format!( + // "line {} of {} (column {})", + // cursor.row + 1, + // last_line + 1, + // cursor.column + 1, + // ); Self { + editor: line_editor, tmp_focus_handle: cx.focus_handle(), } } From 78e1c0f9c37444f79fdbe6475a59558b59fbc1af Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 5 Dec 2023 15:48:41 -0500 Subject: [PATCH 07/27] Implement feedback actions CopySystemSpecsIntoClipboard RequestFeature FileBugReport --- .../feedback2/src/deploy_feedback_button.rs | 8 +- crates/feedback2/src/feedback2.rs | 105 ++++++------- crates/feedback2/src/feedback_editor.rs | 20 +-- crates/feedback2/src/feedback_modal.rs | 9 +- crates/feedback2/src/system_specs.rs | 147 +++++++++--------- crates/gpui2/src/platform.rs | 2 +- crates/workspace2/src/workspace2.rs | 2 +- crates/zed2/src/main.rs | 2 +- 8 files changed, 144 insertions(+), 151 deletions(-) diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs index 800f8d70f8..147db443a5 100644 --- a/crates/feedback2/src/deploy_feedback_button.rs +++ b/crates/feedback2/src/deploy_feedback_button.rs @@ -1,18 +1,18 @@ -use gpui::{Action, AnyElement, Render, ViewContext, WeakView}; +use gpui::{AnyElement, Render, ViewContext, WeakView}; use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip}; use workspace::{StatusItemView, Workspace}; -use crate::{feedback_editor::GiveFeedback, feedback_modal::FeedbackModal}; +use crate::feedback_modal::FeedbackModal; pub struct DeployFeedbackButton { - active: bool, + _active: bool, workspace: WeakView, } impl DeployFeedbackButton { pub fn new(workspace: &Workspace) -> Self { DeployFeedbackButton { - active: false, + _active: false, workspace: workspace.weak_handle(), } } diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index 8bacc4255e..382e449677 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/crates/feedback2/src/feedback2.rs @@ -1,4 +1,7 @@ -use gpui::AppContext; +use gpui::{actions, AppContext, ClipboardItem, PromptLevel}; +use system_specs::SystemSpecs; +// use system_specs::SystemSpecs; +use workspace::Workspace; pub mod deploy_feedback_button; pub mod feedback_editor; @@ -8,60 +11,52 @@ pub mod submit_feedback_button; mod system_specs; +actions!( + CopySystemSpecsIntoClipboard, + FileBugReport, + RequestFeature, + OpenZedCommunityRepo +); + pub fn init(cx: &mut AppContext) { - cx.observe_new_views(feedback_modal::FeedbackModal::register) - .detach(); + // feedback_editor::init(cx); + + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + 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); + }); + }) + .detach(); + + // TODO + // cx.add_global_action(open_zed_community_repo); } -// 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| { -// 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| { -// 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| { -// 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); -// } +pub fn open_zed_community_repo(_: &OpenZedCommunityRepo, cx: &mut AppContext) { + let url = "https://github.com/zed-industries/community"; + cx.open_url(&url); +} diff --git a/crates/feedback2/src/feedback_editor.rs b/crates/feedback2/src/feedback_editor.rs index 396cd27a6a..258b3553f8 100644 --- a/crates/feedback2/src/feedback_editor.rs +++ b/crates/feedback2/src/feedback_editor.rs @@ -33,16 +33,16 @@ actions!(GiveFeedback, SubmitFeedback); // }); // } -#[derive(Serialize)] -struct FeedbackRequestBody<'a> { - feedback_text: &'a str, - email: Option, - metrics_id: Option>, - installation_id: Option>, - system_specs: SystemSpecs, - is_staff: bool, - token: &'a str, -} +// #[derive(Serialize)] +// struct FeedbackRequestBody<'a> { +// feedback_text: &'a str, +// email: Option, +// metrics_id: Option>, +// installation_id: Option>, +// system_specs: SystemSpecs, +// is_staff: bool, +// token: &'a str, +// } // #[derive(Clone)] // pub(crate) struct FeedbackEditor { diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index f04f40541f..1676f350f0 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -1,4 +1,3 @@ -use editor::Editor; use gpui::{ div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, ViewContext, @@ -9,7 +8,7 @@ use workspace::Workspace; use crate::feedback_editor::GiveFeedback; pub struct FeedbackModal { - editor: View, + // editor: View, tmp_focus_handle: FocusHandle, // TODO: should be editor.focus_handle(cx) } @@ -29,8 +28,8 @@ impl FeedbackModal { } pub fn new(cx: &mut ViewContext) -> Self { - let line_editor = cx.build_view(|cx| Editor::single_line(cx)); - let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); + // let line_editor = cx.build_view(|cx| Editor::single_line(cx)); + // let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); // let editor = active_editor.read(cx); // let cursor = editor.selections.last::(cx).head(); @@ -44,7 +43,7 @@ impl FeedbackModal { // cursor.column + 1, // ); Self { - editor: line_editor, + // editor: line_editor, tmp_focus_handle: cx.focus_handle(), } } diff --git a/crates/feedback2/src/system_specs.rs b/crates/feedback2/src/system_specs.rs index 1a2c8775a6..c119acf676 100644 --- a/crates/feedback2/src/system_specs.rs +++ b/crates/feedback2/src/system_specs.rs @@ -1,76 +1,75 @@ -// // 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; +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 { -// // todo!() -// // #[serde(serialize_with = "serialize_app_version")] -// // app_version: Option, -// release_channel: &'static str, -// os_name: &'static str, -// os_version: Option, -// memory: u64, -// architecture: &'static str, +#[derive(Clone, Debug, Serialize)] +pub struct SystemSpecs { + app_version: Option, + release_channel: &'static str, + os_name: &'static str, + os_version: Option, + 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::().dev_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::>() + .join("\n"); + + write!(f, "{system_specs}") + } +} + +// fn serialize_app_version(version: ZED_APP_VERSION, serializer: S) -> Result +// where +// S: serde::Serializer, +// { +// version.map(|v| v.to_string()).serialize(serializer) // } - -// 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::().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::>() -// .join("\n"); - -// write!(f, "{system_specs}") -// } -// } - -// // fn serialize_app_version(version: &Option, serializer: S) -> Result -// // where -// // S: serde::Serializer, -// // { -// // version.map(|v| v.to_string()).serialize(serializer) -// // } diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 651392c9c8..c345ea0643 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -501,7 +501,7 @@ impl Default for CursorStyle { } } -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct SemanticVersion { major: usize, minor: usize, diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 6a82cb8139..6eb8c623b4 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -67,7 +67,7 @@ use std::{ use theme::{ActiveTheme, ThemeSettings}; pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; -use ui::{h_stack, v_stack, StyledExt}; +// use ui::{h_stack, v_stack, StyledExt}; use util::ResultExt; use uuid::Uuid; pub use workspace_settings::{AutosaveSetting, WorkspaceSettings}; diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 6ca5d1a805..2afbdf9fa1 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -221,7 +221,7 @@ fn main() { // language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); - // feedback::init(cx); + feedback::init(cx); welcome::init(cx); // cx.set_menus(menus::menus()); From dec4cfa0b4162372007416e3afb3602424ee7df7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 5 Dec 2023 15:52:02 -0500 Subject: [PATCH 08/27] Implement feedback action OpenZedCommunityRepo --- crates/feedback2/src/feedback2.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index 382e449677..1c58c7c51f 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/crates/feedback2/src/feedback2.rs @@ -1,6 +1,5 @@ use gpui::{actions, AppContext, ClipboardItem, PromptLevel}; use system_specs::SystemSpecs; -// use system_specs::SystemSpecs; use workspace::Workspace; pub mod deploy_feedback_button; @@ -48,15 +47,11 @@ pub fn init(cx: &mut AppContext) { 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(); - - // TODO - // 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.open_url(&url); } From fdc2d7ce426424db35573e7dfbb3f603f033d0f1 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 5 Dec 2023 15:53:52 -0500 Subject: [PATCH 09/27] Remove unused code --- crates/feedback2/src/system_specs.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/feedback2/src/system_specs.rs b/crates/feedback2/src/system_specs.rs index c119acf676..4d61867ba0 100644 --- a/crates/feedback2/src/system_specs.rs +++ b/crates/feedback2/src/system_specs.rs @@ -66,10 +66,3 @@ impl Display for SystemSpecs { write!(f, "{system_specs}") } } - -// fn serialize_app_version(version: ZED_APP_VERSION, serializer: S) -> Result -// where -// S: serde::Serializer, -// { -// version.map(|v| v.to_string()).serialize(serializer) -// } From a27db35ff71fd625003c6e29348f891a8dd0e3b3 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 5 Dec 2023 16:37:43 -0500 Subject: [PATCH 10/27] temp --- crates/feedback2/src/feedback2.rs | 2 +- crates/feedback2/src/feedback_editor.rs | 398 ++++++++++++------------ 2 files changed, 201 insertions(+), 199 deletions(-) diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index 1c58c7c51f..c013b4323e 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/crates/feedback2/src/feedback2.rs @@ -18,7 +18,7 @@ actions!( ); pub fn init(cx: &mut AppContext) { - // feedback_editor::init(cx); + feedback_editor::init(cx); cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace diff --git a/crates/feedback2/src/feedback_editor.rs b/crates/feedback2/src/feedback_editor.rs index 258b3553f8..4726e42e58 100644 --- a/crates/feedback2/src/feedback_editor.rs +++ b/crates/feedback2/src/feedback_editor.rs @@ -1,236 +1,238 @@ -// 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, serde_json, AppContext, Model, PromptLevel, Task, View, ViewContext}; -// use isahc::Request; -// use language::Buffer; -// use postage::prelude::Stream; -// use project::{search::SearchQuery, Project}; -// use regex::Regex; -// use serde::Serialize; -// use std::{ -// ops::{Range, RangeInclusive}, -// sync::Arc, -// }; -// use util::ResultExt; -// use workspace::{searchable::SearchableItem, Workspace}; +use std::{ops::RangeInclusive, sync::Arc}; -// const FEEDBACK_CHAR_LIMIT: RangeInclusive = 10..=5000; -// const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = -// "Feedback failed to submit, see error log for details."; +use anyhow::bail; +use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; +use editor::Editor; +use futures::AsyncReadExt; +use gpui::{ + actions, serde_json, AppContext, Model, PromptLevel, Task, View, ViewContext, VisualContext, +}; +use isahc::Request; +use language::Buffer; +use project::Project; +use regex::Regex; +use serde_derive::Serialize; +use util::ResultExt; +use workspace::Workspace; -use gpui::actions; +use crate::system_specs::SystemSpecs; + +const FEEDBACK_CHAR_LIMIT: RangeInclusive = 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.add_action({ -// move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext| { -// FeedbackEditor::deploy(workspace, cx); -// } -// }); -// } +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, cx| { + workspace.register_action( + move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext| { + FeedbackEditor::deploy(workspace, cx); + }, + ); + }) + .detach(); +} -// #[derive(Serialize)] -// struct FeedbackRequestBody<'a> { -// feedback_text: &'a str, -// email: Option, -// metrics_id: Option>, -// installation_id: Option>, -// system_specs: SystemSpecs, -// is_staff: bool, -// token: &'a str, -// } +#[derive(Serialize)] +struct FeedbackRequestBody<'a> { + feedback_text: &'a str, + email: Option, + metrics_id: Option>, + installation_id: Option>, + system_specs: SystemSpecs, + is_staff: bool, + token: &'a str, +} -// #[derive(Clone)] -// pub(crate) struct FeedbackEditor { -// system_specs: SystemSpecs, -// editor: View, -// project: Model, -// pub allow_submission: bool, -// } +#[derive(Clone)] +pub(crate) struct FeedbackEditor { + system_specs: SystemSpecs, + editor: View, + project: Model, + pub allow_submission: bool, +} -// impl FeedbackEditor { -// fn new( -// system_specs: SystemSpecs, -// project: Model, -// buffer: Model, -// cx: &mut ViewContext, -// ) -> 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 -// }); +impl FeedbackEditor { + fn new( + system_specs: SystemSpecs, + project: Model, + buffer: Model, + cx: &mut ViewContext, + ) -> 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, |_, _, e, cx| cx.emit(e.clone())) -// .detach(); + cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())) + .detach(); -// Self { -// system_specs: system_specs.clone(), -// editor, -// project, -// allow_submission: true, -// } -// } + Self { + system_specs: system_specs.clone(), + editor, + project, + allow_submission: true, + } + } -// pub fn submit(&mut self, cx: &mut ViewContext) -> Task> { -// if !self.allow_submission { -// return Task::ready(Ok(())); -// } + pub fn submit(&mut self, cx: &mut ViewContext) -> Task> { + 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 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 -// }; + 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(())); -// } + 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 mut answer = cx.prompt( + PromptLevel::Info, + "Ready to submit your feedback?", + &["Yes, Submit!", "No"], + ); -// let client = cx.global::>().clone(); -// let specs = self.system_specs.clone(); + let client = cx.global::>().clone(); + let specs = self.system_specs.clone(); -// cx.spawn(|this, mut cx| async move { -// let answer = answer.recv().await; + 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(); + 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::EditorEvent::Closed)) -// .log_err(); -// } + match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await { + Ok(_) => { + this.update(&mut cx, |_, cx| cx.emit(editor::EditorEvent::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(); + 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(())) -// } + Task::ready(Ok(())) + } -// fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext) { -// self.allow_submission = allow_submission; -// cx.notify(); -// } + fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext) { + self.allow_submission = allow_submission; + cx.notify(); + } -// async fn submit_feedback( -// feedback_text: &str, -// zed_client: Arc, -// system_specs: SystemSpecs, -// ) -> anyhow::Result<()> { -// let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL); + async fn submit_feedback( + feedback_text: &str, + zed_client: Arc, + 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 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 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 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 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 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 json_bytes = serde_json::to_vec(&request)?; -// let request = Request::post(feedback_endpoint) -// .header("content-type", "application/json") -// .body(json_bytes.into())?; + 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 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(); + let response_status = response.status(); -// if !response_status.is_success() { -// bail!("Feedback API failed with error: {}", response_status) -// } + if !response_status.is_success() { + bail!("Feedback API failed with error: {}", response_status) + } -// Ok(()) -// } -// } + Ok(()) + } +} -// impl FeedbackEditor { -// pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext) { -// 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 FeedbackEditor { + pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext) { + 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); + } +} // impl View for FeedbackEditor { // fn ui_name() -> &'static str { From eff925cb6a37df6eab2253c4a6904b51e118a224 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 13:26:20 -0500 Subject: [PATCH 11/27] Delete feedback_modal.rs --- crates/feedback2/src/feedback_modal.rs | 216 ------------------------- 1 file changed, 216 deletions(-) delete mode 100644 crates/feedback2/src/feedback_modal.rs diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs deleted file mode 100644 index 1676f350f0..0000000000 --- a/crates/feedback2/src/feedback_modal.rs +++ /dev/null @@ -1,216 +0,0 @@ -use gpui::{ - div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, - ViewContext, -}; -use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; -use workspace::Workspace; - -use crate::feedback_editor::GiveFeedback; - -pub struct FeedbackModal { - // editor: View, - tmp_focus_handle: FocusHandle, // TODO: should be editor.focus_handle(cx) -} - -impl FocusableView for FeedbackModal { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.tmp_focus_handle.clone() - } -} -impl EventEmitter for FeedbackModal {} - -impl FeedbackModal { - pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { - let _handle = cx.view().downgrade(); - workspace.register_action(move |workspace, _: &GiveFeedback, cx| { - workspace.toggle_modal(cx, move |cx| FeedbackModal::new(cx)); - }); - } - - pub fn new(cx: &mut ViewContext) -> Self { - // let line_editor = cx.build_view(|cx| Editor::single_line(cx)); - // let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); - - // let editor = active_editor.read(cx); - // let cursor = editor.selections.last::(cx).head(); - // let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row; - // let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx)); - - // let current_text = format!( - // "line {} of {} (column {})", - // cursor.row + 1, - // last_line + 1, - // cursor.column + 1, - // ); - Self { - // editor: line_editor, - tmp_focus_handle: cx.focus_handle(), - } - } - - // 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, - // event: &editor::EditorEvent, - // cx: &mut ViewContext, - // ) { - // 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) { - // 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(); - // } - // } - - // fn point_from_query(&self, cx: &ViewContext) -> Option { - // 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::().ok())?; - // let column = components.next().and_then(|col| col.parse::().ok()); - // Some(Point::new( - // row.saturating_sub(1), - // column.unwrap_or(0).saturating_sub(1), - // )) - // } - - // fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - // cx.emit(DismissEvent); - // } - - // fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - // 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(); - // } - - // cx.emit(DismissEvent); - // } -} - -impl Render for FeedbackModal { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); - - v_stack() - .elevation_3(cx) - .min_w(rems(40.)) - .max_w(rems(96.)) - .h(rems(40.)) - .p_2() - .gap_2() - .child(h_stack().child(Label::new("Give Feedback").color(Color::Default))) - .child( - div() - .flex_1() - .bg(cx.theme().colors().editor_background) - .border() - .border_color(cx.theme().colors().border) - .child("editor"), - ) - .child( - h_stack() - .justify_end() - .gap_1() - .child( - Button::new("cancel_feedback", "Cancel") - .style(ButtonStyle::Subtle) - .color(Color::Muted) - .on_click(dismiss), - ) - .child( - Button::new("send_feedback", "Send Feedback") - .color(Color::Accent) - .style(ButtonStyle::Filled) - .tooltip(|cx| { - Tooltip::with_meta( - "Submit feedback to the Zed team.", - None, - "Provide an email address if you want us to be able to reply.", - cx, - ) - }), - ), - ) - - // Header - // - has some info, maybe some links - // Body - // - Markdown Editor - // - Email address - // Footer - // - CTA buttons (Send, Cancel) - - // div() - // .elevation_2(cx) - // .key_context( - // "FeedbackModal - // ", - // ) - // .on_action(cx.listener(Self::cancel)) - // .on_action(cx.listener(Self::confirm)) - // .w_96() - // .child( - // v_stack() - // .px_1() - // .pt_0p5() - // .gap_px() - // .child( - // v_stack() - // .py_0p5() - // .px_1() - // .child(div().px_1().py_0p5().child(self.line_editor.clone())), - // ) - // .child( - // div() - // .h_px() - // .w_full() - // .bg(cx.theme().colors().element_background), - // ) - // .child( - // h_stack() - // .justify_between() - // .px_2() - // .py_1() - // .child(Label::new(self.current_text.clone()).color(Color::Muted)), - // ), - // ) - } -} From 4863c9ac25c393f8b84be4c595174075d80fa5d4 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 14:13:44 -0500 Subject: [PATCH 12/27] WIP --- crates/editor2/src/editor.rs | 13 +- .../feedback2/src/deploy_feedback_button.rs | 45 +- crates/feedback2/src/feedback2.rs | 2 +- crates/feedback2/src/feedback_editor.rs | 444 ++++++++++-------- crates/feedback2/src/feedback_info_text.rs | 89 ++-- .../feedback2/src/submit_feedback_button.rs | 211 +++++---- crates/zed2/src/zed2.rs | 14 +- 7 files changed, 441 insertions(+), 377 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 529438648a..7cf50a50e1 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1696,6 +1696,11 @@ impl Editor { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, Self::handle_focus).detach(); cx.on_blur(&focus_handle, Self::handle_blur).detach(); + cx.on_release(|this, cx| { + //todo!() + //cx.emit_global(EditorReleased(self.handle.clone())); + }) + .detach(); let mut this = Self { handle: cx.view().downgrade(), @@ -9240,14 +9245,6 @@ pub struct EditorFocused(pub View); pub struct EditorBlurred(pub View); pub struct EditorReleased(pub WeakView); -// impl Entity for Editor { -// type Event = Event; - -// fn release(&mut self, cx: &mut AppContext) { -// cx.emit_global(EditorReleased(self.handle.clone())); -// } -// } -// impl EventEmitter for Editor {} impl FocusableView for Editor { diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs index 147db443a5..f0f06f5bd0 100644 --- a/crates/feedback2/src/deploy_feedback_button.rs +++ b/crates/feedback2/src/deploy_feedback_button.rs @@ -1,18 +1,18 @@ use gpui::{AnyElement, Render, ViewContext, WeakView}; use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip}; -use workspace::{StatusItemView, Workspace}; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; -use crate::feedback_modal::FeedbackModal; +use crate::feedback_editor::FeedbackEditor; pub struct DeployFeedbackButton { - _active: bool, + active: bool, workspace: WeakView, } impl DeployFeedbackButton { pub fn new(workspace: &Workspace) -> Self { DeployFeedbackButton { - _active: false, + active: false, workspace: workspace.weak_handle(), } } @@ -22,37 +22,34 @@ impl Render for DeployFeedbackButton { type Element = AnyElement; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let is_open = self - .workspace - .upgrade() - .and_then(|workspace| { - workspace.update(cx, |workspace, cx| { - workspace.active_modal::(cx) - }) - }) - .is_some(); + let active = self.active; IconButton::new("give-feedback", Icon::Envelope) .style(ui::ButtonStyle::Subtle) - .selected(is_open) .tooltip(|cx| Tooltip::text("Give Feedback", cx)) - .on_click(cx.listener(|this, _, cx| { + .on_click(cx.listener(move |this, _, cx| { let Some(workspace) = this.workspace.upgrade() else { return; }; - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| FeedbackModal::new(cx)) - }) + + if !active { + workspace.update(cx, |workspace, cx| FeedbackEditor::deploy(workspace, cx)) + } })) .into_any_element() } } + impl StatusItemView for DeployFeedbackButton { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn workspace::item::ItemHandle>, - _cx: &mut ViewContext, - ) { - // no-op + fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + if let Some(item) = item { + if let Some(_) = item.downcast::() { + self.active = true; + cx.notify(); + return; + } + } + self.active = false; + cx.notify(); } } diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index c013b4323e..30caa1aa0e 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/crates/feedback2/src/feedback2.rs @@ -5,7 +5,7 @@ use workspace::Workspace; pub mod deploy_feedback_button; pub mod feedback_editor; pub mod feedback_info_text; -pub mod feedback_modal; +// pub mod feedback_modal; pub mod submit_feedback_button; mod system_specs; diff --git a/crates/feedback2/src/feedback_editor.rs b/crates/feedback2/src/feedback_editor.rs index 4726e42e58..d569566ba6 100644 --- a/crates/feedback2/src/feedback_editor.rs +++ b/crates/feedback2/src/feedback_editor.rs @@ -1,21 +1,29 @@ -use std::{ops::RangeInclusive, sync::Arc}; - +use crate::system_specs::SystemSpecs; use anyhow::bail; use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; -use editor::Editor; +use editor::{Anchor, Editor, EditorEvent}; use futures::AsyncReadExt; use gpui::{ - actions, serde_json, AppContext, Model, PromptLevel, Task, View, ViewContext, VisualContext, + actions, serde_json, AnyElement, AnyView, AppContext, Div, EntityId, EventEmitter, + FocusableView, Model, PromptLevel, Task, View, ViewContext, WindowContext, }; use isahc::Request; -use language::Buffer; -use project::Project; +use language::{Buffer, Event}; +use project::{search::SearchQuery, Project}; use regex::Regex; -use serde_derive::Serialize; +use serde::Serialize; +use std::{ + any::TypeId, + ops::{Range, RangeInclusive}, + sync::Arc, +}; +use ui::{prelude::*, Icon, IconElement, Label}; use util::ResultExt; -use workspace::Workspace; - -use crate::system_specs::SystemSpecs; +use workspace::{ + item::{Item, ItemEvent, ItemHandle}, + searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, + Workspace, +}; const FEEDBACK_CHAR_LIMIT: RangeInclusive = 10..=5000; const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = @@ -24,12 +32,10 @@ const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = actions!(GiveFeedback, SubmitFeedback); pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, cx| { - workspace.register_action( - move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext| { - FeedbackEditor::deploy(workspace, cx); - }, - ); + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &GiveFeedback, cx| { + FeedbackEditor::deploy(workspace, cx); + }); }) .detach(); } @@ -53,6 +59,9 @@ pub(crate) struct FeedbackEditor { pub allow_submission: bool, } +impl EventEmitter for FeedbackEditor {} +impl EventEmitter for FeedbackEditor {} + impl FeedbackEditor { fn new( system_specs: SystemSpecs, @@ -66,8 +75,11 @@ impl FeedbackEditor { editor }); - cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())) - .detach(); + cx.subscribe( + &editor, + |&mut _, _, e: &EditorEvent, cx: &mut ViewContext<_>| cx.emit(e.clone()), + ) + .detach(); Self { system_specs: system_specs.clone(), @@ -101,11 +113,15 @@ impl FeedbackEditor { }; if let Some(error) = error { - cx.prompt(PromptLevel::Critical, &error, &["OK"]); + let prompt = cx.prompt(PromptLevel::Critical, &error, &["OK"]); + cx.spawn(|_, _cx| async move { + prompt.await.ok(); + }) + .detach(); return Task::ready(Ok(())); } - let mut answer = cx.prompt( + let answer = cx.prompt( PromptLevel::Info, "Ready to submit your feedback?", &["Yes, Submit!", "No"], @@ -115,7 +131,7 @@ impl FeedbackEditor { let specs = self.system_specs.clone(); cx.spawn(|this, mut cx| async move { - let answer = answer.recv().await; + let answer = answer.await.ok(); if answer == Some(0) { this.update(&mut cx, |feedback_editor, cx| { @@ -125,18 +141,22 @@ impl FeedbackEditor { match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await { Ok(_) => { - this.update(&mut cx, |_, cx| cx.emit(editor::EditorEvent::Closed)) + this.update(&mut cx, |_, cx| cx.emit(Event::Closed)) .log_err(); } Err(error) => { log::error!("{}", error); this.update(&mut cx, |feedback_editor, cx| { - cx.prompt( + 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(); @@ -234,200 +254,232 @@ impl FeedbackEditor { } } -// impl View for FeedbackEditor { -// fn ui_name() -> &'static str { -// "FeedbackEditor" -// } +// TODO +impl Render for FeedbackEditor { + type Element = Div; -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// ChildView::new(&self.editor, cx).into_any() -// } + fn render(&mut self, _: &mut ViewContext) -> Self::Element { + div().size_full().child(self.editor.clone()) + } +} -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// cx.focus(&self.editor); -// } -// } -// } +impl EventEmitter for FeedbackEditor {} -// impl Entity for FeedbackEditor { -// type Event = editor::Event; -// } +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> { -// Some("Send Feedback".into()) -// } +impl Item for FeedbackEditor { + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some("Send Feedback".into()) + } -// fn tab_content( -// &self, -// _: Option, -// style: &theme::Tab, -// _: &AppContext, -// ) -> AnyElement { -// 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 tab_content(&self, detail: Option, 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(usize, &dyn project::Item)) { -// self.editor.for_each_project_item(cx, f) -// } + 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 is_singleton(&self, _: &AppContext) -> bool { + true + } -// fn can_save(&self, _: &AppContext) -> bool { -// true -// } + fn can_save(&self, _: &AppContext) -> bool { + true + } -// fn save( -// &mut self, -// _: ModelHandle, -// cx: &mut ViewContext, -// ) -> Task> { -// self.submit(cx) -// } + fn save( + &mut self, + _project: Model, + cx: &mut ViewContext, + ) -> Task> { + self.submit(cx) + } -// fn save_as( -// &mut self, -// _: ModelHandle, -// _: std::path::PathBuf, -// cx: &mut ViewContext, -// ) -> Task> { -// self.submit(cx) -// } + fn save_as( + &mut self, + _: Model, + _: std::path::PathBuf, + cx: &mut ViewContext, + ) -> Task> { + self.submit(cx) + } -// fn reload( -// &mut self, -// _: ModelHandle, -// _: &mut ViewContext, -// ) -> Task> { -// Task::Ready(Some(Ok(()))) -// } + fn reload(&mut self, _: Model, _: &mut ViewContext) -> Task> { + Task::Ready(Some(Ok(()))) + } -// fn clone_on_split( -// &self, -// _workspace_id: workspace::WorkspaceId, -// cx: &mut ViewContext, -// ) -> Option -// where -// Self: Sized, -// { -// let buffer = self -// .editor -// .read(cx) -// .buffer() -// .read(cx) -// .as_singleton() -// .expect("Feedback buffer is only ever singleton"); + fn clone_on_split( + &self, + _workspace_id: workspace::WorkspaceId, + cx: &mut ViewContext, + ) -> Option> + 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, -// )) -// } + Some(cx.build_view(|cx| { + Self::new( + self.system_specs.clone(), + self.project.clone(), + buffer.clone(), + cx, + ) + })) + } -// fn as_searchable(&self, handle: &ViewHandle) -> Option> { -// Some(Box::new(handle.clone())) -// } + fn as_searchable(&self, handle: &View) -> Option> { + Some(Box::new(handle.clone())) + } -// fn act_as_type<'a>( -// &'a self, -// type_id: TypeId, -// self_handle: &'a ViewHandle, -// _: &'a AppContext, -// ) -> Option<&'a AnyViewHandle> { -// if type_id == TypeId::of::() { -// Some(self_handle) -// } else if type_id == TypeId::of::() { -// Some(&self.editor) -// } else { -// None -// } -// } + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + cx: &'a AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } -// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { -// Editor::to_item_events(event) -// } -// } + fn deactivated(&mut self, _: &mut ViewContext) {} -// impl SearchableItem for FeedbackEditor { -// type Match = Range; + fn workspace_deactivated(&mut self, _: &mut ViewContext) {} -// fn to_search_event( -// &mut self, -// event: &Self::Event, -// cx: &mut ViewContext, -// ) -> Option { -// self.editor -// .update(cx, |editor, cx| editor.to_search_event(event, cx)) -// } + fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { + false + } -// fn clear_matches(&mut self, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| editor.clear_matches(cx)) -// } + fn tab_description(&self, _: usize, _: &AppContext) -> Option { + None + } -// fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| editor.update_matches(matches, cx)) -// } + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} -// fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { -// self.editor -// .update(cx, |editor, cx| editor.query_suggestion(cx)) -// } + fn is_dirty(&self, _: &AppContext) -> bool { + false + } -// fn activate_match( -// &mut self, -// index: usize, -// matches: Vec, -// cx: &mut ViewContext, -// ) { -// self.editor -// .update(cx, |editor, cx| editor.activate_match(index, matches, cx)) -// } + fn has_conflict(&self, _: &AppContext) -> bool { + false + } -// fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { -// self.editor -// .update(cx, |e, cx| e.select_matches(matches, cx)) -// } -// fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext) { -// self.editor -// .update(cx, |e, cx| e.replace(matches, query, cx)); -// } -// fn find_matches( -// &mut self, -// query: Arc, -// cx: &mut ViewContext, -// ) -> Task> { -// self.editor -// .update(cx, |editor, cx| editor.find_matches(query, cx)) -// } + fn breadcrumb_location(&self) -> workspace::ToolbarItemLocation { + workspace::ToolbarItemLocation::Hidden + } -// fn active_match_index( -// &mut self, -// matches: Vec, -// cx: &mut ViewContext, -// ) -> Option { -// self.editor -// .update(cx, |editor, cx| editor.active_match_index(matches, cx)) -// } -// } + fn breadcrumbs( + &self, + _theme: &theme::Theme, + _cx: &AppContext, + ) -> Option> { + None + } + + fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} + + fn serialized_item_kind() -> Option<&'static str> { + Some("feedback") + } + + fn deserialize( + _project: gpui::Model, + _workspace: gpui::WeakView, + _workspace_id: workspace::WorkspaceId, + _item_id: workspace::ItemId, + _cx: &mut ViewContext, + ) -> Task>> { + 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> { + None + } +} + +impl EventEmitter for FeedbackEditor {} + +impl SearchableItem for FeedbackEditor { + type Match = Range; + + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| editor.clear_matches(cx)) + } + + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| editor.update_matches(matches, cx)) + } + + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + self.editor + .update(cx, |editor, cx| editor.query_suggestion(cx)) + } + + fn activate_match( + &mut self, + index: usize, + matches: Vec, + cx: &mut ViewContext, + ) { + self.editor + .update(cx, |editor, cx| editor.activate_match(index, matches, cx)) + } + + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.select_matches(matches, cx)) + } + fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.replace(matches, query, cx)); + } + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> Task> { + self.editor + .update(cx, |editor, cx| editor.find_matches(query, cx)) + } + + fn active_match_index( + &mut self, + matches: Vec, + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |editor, cx| editor.active_match_index(matches, cx)) + } +} diff --git a/crates/feedback2/src/feedback_info_text.rs b/crates/feedback2/src/feedback_info_text.rs index 96c76929ea..643e5c7b0c 100644 --- a/crates/feedback2/src/feedback_info_text.rs +++ b/crates/feedback2/src/feedback_info_text.rs @@ -1,28 +1,36 @@ -// use gpui::{ -// elements::{Flex, Label, MouseEventHandler, ParentElement, Text}, -// platform::{CursorStyle, MouseButton}, -// AnyElement, Element, Entity, View, ViewContext, ViewHandle, -// }; -// use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; +use gpui::{Div, EventEmitter, View, ViewContext}; +use ui::{prelude::*, Label}; +use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; -// use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo}; +use crate::feedback_editor::FeedbackEditor; -// pub struct FeedbackInfoText { -// active_item: Option>, -// } +pub struct FeedbackInfoText { + active_item: Option>, +} -// impl FeedbackInfoText { -// pub fn new() -> Self { -// Self { -// active_item: Default::default(), -// } -// } -// } +impl FeedbackInfoText { + pub fn new() -> Self { + Self { + active_item: Default::default(), + } + } +} -// impl Entity for FeedbackInfoText { -// type Event = (); -// } +// TODO +impl Render for FeedbackInfoText { + type Element = Div; + fn render(&mut self, _: &mut ViewContext) -> 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" @@ -73,22 +81,25 @@ // } // } -// impl ToolbarItemView for FeedbackInfoText { -// fn set_active_pane_item( -// &mut self, -// active_pane_item: Option<&dyn ItemHandle>, -// cx: &mut ViewContext, -// ) -> workspace::ToolbarItemLocation { -// cx.notify(); -// if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) -// { -// self.active_item = Some(feedback_editor); -// ToolbarItemLocation::PrimaryLeft { -// flex: Some((1., false)), -// } -// } else { -// self.active_item = None; -// ToolbarItemLocation::Hidden -// } -// } -// } +impl EventEmitter for FeedbackInfoText {} + +impl ToolbarItemView for FeedbackInfoText { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + cx.notify(); + + if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) + { + dbg!("Editor"); + self.active_item = Some(feedback_editor); + ToolbarItemLocation::PrimaryLeft + } else { + dbg!("no editor"); + self.active_item = None; + ToolbarItemLocation::Hidden + } + } +} diff --git a/crates/feedback2/src/submit_feedback_button.rs b/crates/feedback2/src/submit_feedback_button.rs index 78a66619a2..220a6d3406 100644 --- a/crates/feedback2/src/submit_feedback_button.rs +++ b/crates/feedback2/src/submit_feedback_button.rs @@ -1,112 +1,115 @@ -// use crate::{feedback_editor::SubmitFeedback, feedback_modal::FeedbackModal}; -// use anyhow::Result; -// use gpui::{AppContext, Render, Task, View, ViewContext}; -// use ui::IconButton; -// use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; +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_async_action(SubmitFeedbackButton::submit); -// } +pub fn init(cx: &mut AppContext) { + // cx.add_action(SubmitFeedbackButton::submit); +} -// pub struct SubmitFeedbackButton { -// pub(crate) active_item: Option>, -// } +pub struct SubmitFeedbackButton { + pub(crate) active_item: Option>, +} -// impl SubmitFeedbackButton { -// pub fn new() -> Self { -// Self { -// active_item: Default::default(), -// } -// } +impl SubmitFeedbackButton { + pub fn new() -> Self { + Self { + active_item: Default::default(), + } + } -// pub fn submit( -// &mut self, -// _: &SubmitFeedback, -// cx: &mut ViewContext, -// ) -> Option>> { -// if let Some(active_item) = self.active_item.as_ref() { -// Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.submit(cx))) -// } else { -// None -// } + pub fn submit( + &mut self, + _: &SubmitFeedback, + cx: &mut ViewContext, + ) -> Option>> { + 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::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) -> AnyElement { +// 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::(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::( +// 0, +// "cmd-s", +// Some(Box::new(SubmitFeedback)), +// theme.tooltip.clone(), +// cx, +// ) +// .into_any() // } // } -// impl Render for SubmitFeedbackbutton { -// type Element; +impl EventEmitter for SubmitFeedbackButton {} -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// todo!(); -// // IconButton::new("give-feedback", Icon::Envelope) -// // .style(ui::ButtonStyle::Subtle) -// // .on_click(|_, cx| cx.dispatch_action(GiveFeedback)) -// } -// } - -// // impl View for SubmitFeedbackButton { -// // fn ui_name() -> &'static str { -// // "SubmitFeedbackButton" -// // } - -// // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// // 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::(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::( -// // 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, -// ) -> workspace::ToolbarItemLocation { -// cx.notify(); -// if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) -// { -// self.active_item = Some(feedback_editor); -// ToolbarItemLocation::PrimaryRight { flex: None } -// } else { -// self.active_item = None; -// ToolbarItemLocation::Hidden -// } -// } -// } +impl ToolbarItemView for SubmitFeedbackButton { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + cx.notify(); + if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) + { + self.active_item = Some(feedback_editor); + ToolbarItemLocation::PrimaryRight + } else { + self.active_item = None; + ToolbarItemLocation::Hidden + } + } +} diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 9ce1966a7a..c4d5831589 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -10,6 +10,9 @@ pub use assets::*; use breadcrumbs::Breadcrumbs; use collections::VecDeque; use editor::{Editor, MultiBuffer}; +use feedback::{ + feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton, +}; use gpui::{ actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, @@ -110,11 +113,12 @@ pub fn initialize_workspace(app_state: Arc, 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 feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); - // toolbar.add_item(feedback_info_text, cx); + let submit_feedback_button = + cx.build_view(|_| SubmitFeedbackButton::new()); + // todo!(tool bar does not display or fire correctly right now, this is only stubbed in) + toolbar.add_item(submit_feedback_button, cx); + let feedback_info_text = cx.build_view(|_| FeedbackInfoText::new()); + toolbar.add_item(feedback_info_text, cx); // let lsp_log_item = // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); // toolbar.add_item(lsp_log_item, cx); From 02a6a2e1a3f892c668ee6a1e3e1d6e5328408f22 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 14:13:56 -0500 Subject: [PATCH 13/27] Revert "Delete feedback_modal.rs" This reverts commit eff925cb6a37df6eab2253c4a6904b51e118a224. --- crates/feedback2/src/feedback_modal.rs | 216 +++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 crates/feedback2/src/feedback_modal.rs diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs new file mode 100644 index 0000000000..1676f350f0 --- /dev/null +++ b/crates/feedback2/src/feedback_modal.rs @@ -0,0 +1,216 @@ +use gpui::{ + div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, + ViewContext, +}; +use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; +use workspace::Workspace; + +use crate::feedback_editor::GiveFeedback; + +pub struct FeedbackModal { + // editor: View, + tmp_focus_handle: FocusHandle, // TODO: should be editor.focus_handle(cx) +} + +impl FocusableView for FeedbackModal { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.tmp_focus_handle.clone() + } +} +impl EventEmitter for FeedbackModal {} + +impl FeedbackModal { + pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { + let _handle = cx.view().downgrade(); + workspace.register_action(move |workspace, _: &GiveFeedback, cx| { + workspace.toggle_modal(cx, move |cx| FeedbackModal::new(cx)); + }); + } + + pub fn new(cx: &mut ViewContext) -> Self { + // let line_editor = cx.build_view(|cx| Editor::single_line(cx)); + // let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); + + // let editor = active_editor.read(cx); + // let cursor = editor.selections.last::(cx).head(); + // let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row; + // let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx)); + + // let current_text = format!( + // "line {} of {} (column {})", + // cursor.row + 1, + // last_line + 1, + // cursor.column + 1, + // ); + Self { + // editor: line_editor, + tmp_focus_handle: cx.focus_handle(), + } + } + + // 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, + // event: &editor::EditorEvent, + // cx: &mut ViewContext, + // ) { + // 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) { + // 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(); + // } + // } + + // fn point_from_query(&self, cx: &ViewContext) -> Option { + // 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::().ok())?; + // let column = components.next().and_then(|col| col.parse::().ok()); + // Some(Point::new( + // row.saturating_sub(1), + // column.unwrap_or(0).saturating_sub(1), + // )) + // } + + // fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + // cx.emit(DismissEvent); + // } + + // fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // 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(); + // } + + // cx.emit(DismissEvent); + // } +} + +impl Render for FeedbackModal { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); + + v_stack() + .elevation_3(cx) + .min_w(rems(40.)) + .max_w(rems(96.)) + .h(rems(40.)) + .p_2() + .gap_2() + .child(h_stack().child(Label::new("Give Feedback").color(Color::Default))) + .child( + div() + .flex_1() + .bg(cx.theme().colors().editor_background) + .border() + .border_color(cx.theme().colors().border) + .child("editor"), + ) + .child( + h_stack() + .justify_end() + .gap_1() + .child( + Button::new("cancel_feedback", "Cancel") + .style(ButtonStyle::Subtle) + .color(Color::Muted) + .on_click(dismiss), + ) + .child( + Button::new("send_feedback", "Send Feedback") + .color(Color::Accent) + .style(ButtonStyle::Filled) + .tooltip(|cx| { + Tooltip::with_meta( + "Submit feedback to the Zed team.", + None, + "Provide an email address if you want us to be able to reply.", + cx, + ) + }), + ), + ) + + // Header + // - has some info, maybe some links + // Body + // - Markdown Editor + // - Email address + // Footer + // - CTA buttons (Send, Cancel) + + // div() + // .elevation_2(cx) + // .key_context( + // "FeedbackModal + // ", + // ) + // .on_action(cx.listener(Self::cancel)) + // .on_action(cx.listener(Self::confirm)) + // .w_96() + // .child( + // v_stack() + // .px_1() + // .pt_0p5() + // .gap_px() + // .child( + // v_stack() + // .py_0p5() + // .px_1() + // .child(div().px_1().py_0p5().child(self.line_editor.clone())), + // ) + // .child( + // div() + // .h_px() + // .w_full() + // .bg(cx.theme().colors().element_background), + // ) + // .child( + // h_stack() + // .justify_between() + // .px_2() + // .py_1() + // .child(Label::new(self.current_text.clone()).color(Color::Muted)), + // ), + // ) + } +} From f4c7b133974b4b219ddfd8fb850c1ef05402b64d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 16:15:44 -0500 Subject: [PATCH 14/27] temp --- .../feedback2/src/deploy_feedback_button.rs | 42 ++- crates/feedback2/src/feedback2.rs | 6 +- crates/feedback2/src/feedback_modal.rs | 243 +++++++++++------- crates/zed2/src/zed2.rs | 7 +- 4 files changed, 177 insertions(+), 121 deletions(-) diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs index f0f06f5bd0..c4ab36005a 100644 --- a/crates/feedback2/src/deploy_feedback_button.rs +++ b/crates/feedback2/src/deploy_feedback_button.rs @@ -2,7 +2,7 @@ use gpui::{AnyElement, Render, ViewContext, WeakView}; use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, Workspace}; -use crate::feedback_editor::FeedbackEditor; +use crate::{feedback_editor::GiveFeedback, feedback_modal::FeedbackModal}; pub struct DeployFeedbackButton { active: bool, @@ -22,34 +22,32 @@ impl Render for DeployFeedbackButton { type Element = AnyElement; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let active = self.active; - + let is_open = self + .workspace + .upgrade() + .and_then(|workspace| { + workspace.update(cx, |workspace, cx| { + workspace.active_modal::(cx) + }) + }) + .is_some(); IconButton::new("give-feedback", Icon::Envelope) .style(ui::ButtonStyle::Subtle) + .selected(is_open) .tooltip(|cx| Tooltip::text("Give Feedback", cx)) - .on_click(cx.listener(move |this, _, cx| { - let Some(workspace) = this.workspace.upgrade() else { - return; - }; - - if !active { - workspace.update(cx, |workspace, cx| FeedbackEditor::deploy(workspace, 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) { - if let Some(item) = item { - if let Some(_) = item.downcast::() { - 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, + ) { + // no-op } } diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index 30caa1aa0e..19c26e4f1c 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/crates/feedback2/src/feedback2.rs @@ -5,7 +5,7 @@ use workspace::Workspace; pub mod deploy_feedback_button; pub mod feedback_editor; pub mod feedback_info_text; -// pub mod feedback_modal; +pub mod feedback_modal; pub mod submit_feedback_button; mod system_specs; @@ -18,7 +18,9 @@ actions!( ); pub fn init(cx: &mut AppContext) { - feedback_editor::init(cx); + // 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, _cx| { workspace diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 1676f350f0..3694a25710 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -1,20 +1,34 @@ -use gpui::{ - div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Render, - ViewContext, -}; -use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; -use workspace::Workspace; +use std::ops::RangeInclusive; -use crate::feedback_editor::GiveFeedback; +use editor::{Editor, EditorEvent}; +use gpui::{ + div, red, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model, + Render, View, ViewContext, +}; +use language::Buffer; +use project::Project; +use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; +use util::ResultExt; +use workspace::{item::Item, Workspace}; + +use crate::{feedback_editor::GiveFeedback, system_specs::SystemSpecs, OpenZedCommunityRepo}; + +const FEEDBACK_CHAR_LIMIT: RangeInclusive = 10..=5000; +const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = + "Feedback failed to submit, see error log for details."; pub struct FeedbackModal { - // editor: View, - tmp_focus_handle: FocusHandle, // TODO: should be editor.focus_handle(cx) + system_specs: SystemSpecs, + feedback_editor: View, + email_address_editor: View, + project: Model, + pub allow_submission: bool, + character_count: usize, } impl FocusableView for FeedbackModal { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.tmp_focus_handle.clone() + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.feedback_editor.focus_handle(cx) } } impl EventEmitter for FeedbackModal {} @@ -23,28 +37,78 @@ impl FeedbackModal { pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { let _handle = cx.view().downgrade(); workspace.register_action(move |workspace, _: &GiveFeedback, cx| { - workspace.toggle_modal(cx, move |cx| FeedbackModal::new(cx)); + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + + let project = workspace.project().clone(); + + cx.spawn(|workspace, mut cx| async move { + let markdown = markdown.await.log_err(); + let buffer = project + .update(&mut cx, |project, cx| { + project.create_buffer("", markdown, cx) + })? + .expect("creating buffers on a local workspace always succeeds"); + + workspace.update(&mut cx, |workspace, cx| { + let system_specs = SystemSpecs::new(cx); + + workspace.toggle_modal(cx, move |cx| { + FeedbackModal::new(system_specs, project, buffer, cx) + }); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); }); } - pub fn new(cx: &mut ViewContext) -> Self { - // let line_editor = cx.build_view(|cx| Editor::single_line(cx)); - // let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); + pub fn new( + system_specs: SystemSpecs, + project: Model, + buffer: Model, + cx: &mut ViewContext, + ) -> Self { + let email_address_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("Email address (optional)", 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); + editor + }); - // let editor = active_editor.read(cx); - // let cursor = editor.selections.last::(cx).head(); - // let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row; - // let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx)); + cx.subscribe( + &feedback_editor, + |this, editor, event: &EditorEvent, cx| match event { + EditorEvent::Edited => { + this.character_count = editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("Feedback editor is never a multi-buffer") + .read(cx) + .len(); + cx.notify(); + } + _ => {} + }, + ) + .detach(); - // let current_text = format!( - // "line {} of {} (column {})", - // cursor.row + 1, - // last_line + 1, - // cursor.column + 1, - // ); Self { - // editor: line_editor, - tmp_focus_handle: cx.focus_handle(), + system_specs: system_specs.clone(), + feedback_editor, + email_address_editor, + project, + allow_submission: true, + character_count: 0, } } @@ -127,7 +191,13 @@ impl Render for FeedbackModal { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let character_count_error = (self.character_count < *FEEDBACK_CHAR_LIMIT.start()) + || (self.character_count > *FEEDBACK_CHAR_LIMIT.end()); + let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); + // let open_community_issues = + // cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); + // let open_community_discussions = cx.listener(|_, _, cx| cx.emit(DismissEvent)); v_stack() .elevation_3(cx) @@ -136,81 +206,72 @@ impl Render for FeedbackModal { .h(rems(40.)) .p_2() .gap_2() - .child(h_stack().child(Label::new("Give Feedback").color(Color::Default))) + .child( + v_stack().child( + div() + .size_full() + .border() + .border_color(red()) + .child(Label::new("Give Feedback").color(Color::Default)) + .child(Label::new("This editor supports markdown").color(Color::Muted)), + ), + ) .child( div() .flex_1() .bg(cx.theme().colors().editor_background) .border() .border_color(cx.theme().colors().border) - .child("editor"), + .child(self.feedback_editor.clone()), + ) + .child( + div().border().border_color(red()).child( + Label::new(format!( + "{} / {} Characters", + self.character_count, + FEEDBACK_CHAR_LIMIT.end() + )) + .color(Color::Default), + ), + ) + .child( div() + .bg(cx.theme().colors().editor_background) + .border() + .border_color(cx.theme().colors().border) + .child(self.email_address_editor.clone()) ) .child( h_stack() - .justify_end() + .justify_between() .gap_1() - .child( - Button::new("cancel_feedback", "Cancel") - .style(ButtonStyle::Subtle) - .color(Color::Muted) - .on_click(dismiss), + .child(Button::new("community_repo", "Community Repo") + .style(ButtonStyle::Filled) + .color(Color::Muted) + // .on_click(cx.dispatch_action(Box::new(OpenZedCommunityRepo))) ) - .child( - Button::new("send_feedback", "Send Feedback") - .color(Color::Accent) - .style(ButtonStyle::Filled) - .tooltip(|cx| { - Tooltip::with_meta( - "Submit feedback to the Zed team.", - None, - "Provide an email address if you want us to be able to reply.", - cx, - ) - }), - ), + .child(h_stack().justify_between().gap_1() + .child( + Button::new("cancel_feedback", "Cancel") + .style(ButtonStyle::Subtle) + .color(Color::Muted) + .on_click(dismiss), + ) + .child( + Button::new("send_feedback", "Send Feedback") + .color(Color::Accent) + .style(ButtonStyle::Filled) + .tooltip(|cx| { + Tooltip::with_meta( + "Submit feedback to the Zed team.", + None, + "Provide an email address if you want us to be able to reply.", + cx, + ) + }) + .when(character_count_error, |this| this.disabled(true)), + ), + ) + ) - - // Header - // - has some info, maybe some links - // Body - // - Markdown Editor - // - Email address - // Footer - // - CTA buttons (Send, Cancel) - - // div() - // .elevation_2(cx) - // .key_context( - // "FeedbackModal - // ", - // ) - // .on_action(cx.listener(Self::cancel)) - // .on_action(cx.listener(Self::confirm)) - // .w_96() - // .child( - // v_stack() - // .px_1() - // .pt_0p5() - // .gap_px() - // .child( - // v_stack() - // .py_0p5() - // .px_1() - // .child(div().px_1().py_0p5().child(self.line_editor.clone())), - // ) - // .child( - // div() - // .h_px() - // .w_full() - // .bg(cx.theme().colors().element_background), - // ) - // .child( - // h_stack() - // .justify_between() - // .px_2() - // .py_1() - // .child(Label::new(self.current_text.clone()).color(Color::Muted)), - // ), - // ) } } diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index c4d5831589..f3f8d3b9f4 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -10,9 +10,7 @@ pub use assets::*; use breadcrumbs::Breadcrumbs; use collections::VecDeque; use editor::{Editor, MultiBuffer}; -use feedback::{ - feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton, -}; +use feedback::submit_feedback_button::SubmitFeedbackButton; use gpui::{ actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, @@ -115,10 +113,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // toolbar.add_item(project_search_bar, cx); let submit_feedback_button = cx.build_view(|_| SubmitFeedbackButton::new()); - // todo!(tool bar does not display or fire correctly right now, this is only stubbed in) toolbar.add_item(submit_feedback_button, cx); - let feedback_info_text = cx.build_view(|_| FeedbackInfoText::new()); - toolbar.add_item(feedback_info_text, cx); // let lsp_log_item = // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); // toolbar.add_item(lsp_log_item, cx); From 46c998ca8d311c919fcf02ff33357808633657ac Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 17:27:10 -0500 Subject: [PATCH 15/27] WIP --- Cargo.lock | 1 + crates/editor2/src/editor.rs | 11 + crates/feedback2/Cargo.toml | 1 + .../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 +++++--- .../feedback2/src/submit_feedback_button.rs | 115 ----- crates/zed2/src/zed2.rs | 4 - 10 files changed, 146 insertions(+), 795 deletions(-) delete mode 100644 crates/feedback2/src/feedback_editor.rs delete mode 100644 crates/feedback2/src/feedback_info_text.rs delete mode 100644 crates/feedback2/src/submit_feedback_button.rs diff --git a/Cargo.lock b/Cargo.lock index c2efbdd863..b156b29145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3186,6 +3186,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client2", + "db2", "editor2", "futures 0.3.28", "gpui2", diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 7cf50a50e1..cdaa42972e 100644 --- a/crates/editor2/src/editor.rs +++ b/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 { + 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>, cx: &mut ViewContext) { self.transact(cx, |this, cx| { this.buffer diff --git a/crates/feedback2/Cargo.toml b/crates/feedback2/Cargo.toml index fbf033919d..6360bd193f 100644 --- a/crates/feedback2/Cargo.toml +++ b/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" } diff --git a/crates/feedback2/src/deploy_feedback_button.rs b/crates/feedback2/src/deploy_feedback_button.rs index c4ab36005a..e5884cf9b1 100644 --- a/crates/feedback2/src/deploy_feedback_button.rs +++ b/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, } 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, ) { - // no-op } } diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index 19c26e4f1c..63a1e86211 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/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; diff --git a/crates/feedback2/src/feedback_editor.rs b/crates/feedback2/src/feedback_editor.rs deleted file mode 100644 index d569566ba6..0000000000 --- a/crates/feedback2/src/feedback_editor.rs +++ /dev/null @@ -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 = 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, - metrics_id: Option>, - installation_id: Option>, - system_specs: SystemSpecs, - is_staff: bool, - token: &'a str, -} - -#[derive(Clone)] -pub(crate) struct FeedbackEditor { - system_specs: SystemSpecs, - editor: View, - project: Model, - pub allow_submission: bool, -} - -impl EventEmitter for FeedbackEditor {} -impl EventEmitter for FeedbackEditor {} - -impl FeedbackEditor { - fn new( - system_specs: SystemSpecs, - project: Model, - buffer: Model, - cx: &mut ViewContext, - ) -> 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) -> Task> { - 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::>().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.allow_submission = allow_submission; - cx.notify(); - } - - async fn submit_feedback( - feedback_text: &str, - zed_client: Arc, - 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) { - 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::Element { - div().size_full().child(self.editor.clone()) - } -} - -impl EventEmitter 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 { - Some("Send Feedback".into()) - } - - fn tab_content(&self, detail: Option, 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, - cx: &mut ViewContext, - ) -> Task> { - self.submit(cx) - } - - fn save_as( - &mut self, - _: Model, - _: std::path::PathBuf, - cx: &mut ViewContext, - ) -> Task> { - self.submit(cx) - } - - fn reload(&mut self, _: Model, _: &mut ViewContext) -> Task> { - Task::Ready(Some(Ok(()))) - } - - fn clone_on_split( - &self, - _workspace_id: workspace::WorkspaceId, - cx: &mut ViewContext, - ) -> Option> - 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) -> Option> { - Some(Box::new(handle.clone())) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a View, - cx: &'a AppContext, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.to_any()) - } else if type_id == TypeId::of::() { - Some(self.editor.to_any()) - } else { - None - } - } - - fn deactivated(&mut self, _: &mut ViewContext) {} - - fn workspace_deactivated(&mut self, _: &mut ViewContext) {} - - fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { - false - } - - fn tab_description(&self, _: usize, _: &AppContext) -> Option { - None - } - - fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) {} - - 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> { - None - } - - fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} - - fn serialized_item_kind() -> Option<&'static str> { - Some("feedback") - } - - fn deserialize( - _project: gpui::Model, - _workspace: gpui::WeakView, - _workspace_id: workspace::WorkspaceId, - _item_id: workspace::ItemId, - _cx: &mut ViewContext, - ) -> Task>> { - 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> { - None - } -} - -impl EventEmitter for FeedbackEditor {} - -impl SearchableItem for FeedbackEditor { - type Match = Range; - - fn clear_matches(&mut self, cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| editor.clear_matches(cx)) - } - - fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| editor.update_matches(matches, cx)) - } - - fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - self.editor - .update(cx, |editor, cx| editor.query_suggestion(cx)) - } - - fn activate_match( - &mut self, - index: usize, - matches: Vec, - cx: &mut ViewContext, - ) { - self.editor - .update(cx, |editor, cx| editor.activate_match(index, matches, cx)) - } - - fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - self.editor - .update(cx, |e, cx| e.select_matches(matches, cx)) - } - fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext) { - self.editor - .update(cx, |e, cx| e.replace(matches, query, cx)); - } - fn find_matches( - &mut self, - query: Arc, - cx: &mut ViewContext, - ) -> Task> { - self.editor - .update(cx, |editor, cx| editor.find_matches(query, cx)) - } - - fn active_match_index( - &mut self, - matches: Vec, - cx: &mut ViewContext, - ) -> Option { - self.editor - .update(cx, |editor, cx| editor.active_match_index(matches, cx)) - } -} diff --git a/crates/feedback2/src/feedback_info_text.rs b/crates/feedback2/src/feedback_info_text.rs deleted file mode 100644 index 643e5c7b0c..0000000000 --- a/crates/feedback2/src/feedback_info_text.rs +++ /dev/null @@ -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>, -} - -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::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) -> AnyElement { -// 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::(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 for FeedbackInfoText {} - -impl ToolbarItemView for FeedbackInfoText { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> workspace::ToolbarItemLocation { - cx.notify(); - - if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) - { - dbg!("Editor"); - self.active_item = Some(feedback_editor); - ToolbarItemLocation::PrimaryLeft - } else { - dbg!("no editor"); - self.active_item = None; - ToolbarItemLocation::Hidden - } - } -} diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 3694a25710..ecbb242ed2 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/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 = 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, + metrics_id: Option>, + installation_id: Option>, + system_specs: SystemSpecs, + is_staff: bool, + token: &'a str, +} + pub struct FeedbackModal { system_specs: SystemSpecs, feedback_editor: View, email_address_editor: View, project: Model, - 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(); - // }) - // } + pub fn submit(&mut self, cx: &mut ViewContext) -> Task> { + 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 on_feedback_editor_event( - // &mut self, - // _: View, - // event: &editor::EditorEvent, - // cx: &mut ViewContext, - // ) { - // match event { - // // todo!() this isn't working... - // editor::EditorEvent::Blurred => cx.emit(DismissEvent), - // editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), - // _ => {} - // } - // } + if let Some(email) = email.clone() { + cx.spawn(|_, _| KEY_VALUE_STORE.write_kvp(DATABASE_KEY_NAME.to_string(), email.clone())) + .detach() + } - // fn highlight_current_line(&mut self, cx: &mut ViewContext) { - // 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(); - // } - // } + let answer = cx.prompt( + PromptLevel::Info, + "Ready to submit your feedback?", + &["Yes, Submit!", "No"], + ); + let client = cx.global::>().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 point_from_query(&self, cx: &ViewContext) -> Option { - // 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::().ok())?; - // let column = components.next().and_then(|col| col.parse::().ok()); - // Some(Point::new( - // row.saturating_sub(1), - // column.unwrap_or(0).saturating_sub(1), - // )) - // } + fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext) { + self.pending_submission = pending_submission; + cx.notify(); + } - // fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - // cx.emit(DismissEvent); - // } - - // fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - // 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(); - // } - - // cx.emit(DismissEvent); - // } + async fn submit_feedback( + feedback_text: &str, + email: Option, + zed_client: Arc, + 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::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)), ), ) diff --git a/crates/feedback2/src/submit_feedback_button.rs b/crates/feedback2/src/submit_feedback_button.rs deleted file mode 100644 index 220a6d3406..0000000000 --- a/crates/feedback2/src/submit_feedback_button.rs +++ /dev/null @@ -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>, -} - -impl SubmitFeedbackButton { - pub fn new() -> Self { - Self { - active_item: Default::default(), - } - } - - pub fn submit( - &mut self, - _: &SubmitFeedback, - cx: &mut ViewContext, - ) -> Option>> { - 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::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) -> AnyElement { -// 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::(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::( -// 0, -// "cmd-s", -// Some(Box::new(SubmitFeedback)), -// theme.tooltip.clone(), -// cx, -// ) -// .into_any() -// } -// } - -impl EventEmitter for SubmitFeedbackButton {} - -impl ToolbarItemView for SubmitFeedbackButton { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> workspace::ToolbarItemLocation { - cx.notify(); - if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::()) - { - self.active_item = Some(feedback_editor); - ToolbarItemLocation::PrimaryRight - } else { - self.active_item = None; - ToolbarItemLocation::Hidden - } - } -} diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index f3f8d3b9f4..b3e521850a 100644 --- a/crates/zed2/src/zed2.rs +++ b/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, 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); From 110612bf263053db3713a61bf793f830a1abbcd7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 18:36:17 -0500 Subject: [PATCH 16/27] Reduce amount of state being stored --- crates/feedback2/src/feedback_modal.rs | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index ecbb242ed2..88cee2852b 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -43,8 +43,7 @@ pub struct FeedbackModal { email_address_editor: View, project: Model, character_count: usize, - allow_submission: bool, - pub pending_submission: bool, + pending_submission: bool, } impl FocusableView for FeedbackModal { @@ -134,24 +133,15 @@ impl FeedbackModal { feedback_editor, email_address_editor, project, - allow_submission: false, pending_submission: false, character_count: 0, } } pub fn submit(&mut self, cx: &mut ViewContext) -> Task> { - 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); - if let Some(email) = email.clone() { - cx.spawn(|_, _| KEY_VALUE_STORE.write_kvp(DATABASE_KEY_NAME.to_string(), email.clone())) - .detach() - } - let answer = cx.prompt( PromptLevel::Info, "Ready to submit your feedback?", @@ -162,6 +152,12 @@ impl FeedbackModal { cx.spawn(|this, mut cx| async move { let answer = answer.await.ok(); if answer == Some(0) { + if let Some(email) = email.clone() { + let _ = KEY_VALUE_STORE + .write_kvp(DATABASE_KEY_NAME.to_string(), email) + .await; + } + this.update(&mut cx, |feedback_editor, cx| { feedback_editor.set_pending_submission(true, cx); }) @@ -241,7 +237,7 @@ impl Render for FeedbackModal { None => true, }; - self.allow_submission = FEEDBACK_CHAR_LIMIT.contains(&self.character_count) + let allow_submission = FEEDBACK_CHAR_LIMIT.contains(&self.character_count) && valid_email_address && !self.pending_submission; @@ -311,6 +307,11 @@ impl Render for FeedbackModal { Button::new("send_feedback", "Send Feedback") .color(Color::Accent) .style(ButtonStyle::Filled) + // .on_click(|_, cx| { + // cx.build_view(|cx, this| { + // FeedbackModal::submit(cx) + // }) + // }) .tooltip(|cx| { Tooltip::with_meta( "Submit feedback to the Zed team.", @@ -319,7 +320,7 @@ impl Render for FeedbackModal { cx, ) }) - .when(!self.allow_submission, |this| this.disabled(true)), + .when(!allow_submission, |this| this.disabled(true)), ), ) From 0885fa67fc2aae0fee2d954c445c38842dfb9d61 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 19:10:45 -0500 Subject: [PATCH 17/27] Hook up submit --- crates/feedback2/src/feedback_modal.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 88cee2852b..f67fa2f133 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -307,11 +307,8 @@ impl Render for FeedbackModal { Button::new("send_feedback", "Send Feedback") .color(Color::Accent) .style(ButtonStyle::Filled) - // .on_click(|_, cx| { - // cx.build_view(|cx, this| { - // FeedbackModal::submit(cx) - // }) - // }) + // TODO - error handling - show modal on error + .on_click(cx.listener(|this, _, cx| {let _ = this.submit(cx);})) .tooltip(|cx| { Tooltip::with_meta( "Submit feedback to the Zed team.", From 034c11cc96dc1e1995ab06e3820a183b6291e1eb Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 19:11:03 -0500 Subject: [PATCH 18/27] Hook up community repo button --- crates/feedback2/src/feedback_modal.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index f67fa2f133..50bd75f6eb 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -245,6 +245,8 @@ impl Render for FeedbackModal { // let open_community_issues = // cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); // let open_community_discussions = cx.listener(|_, _, cx| cx.emit(DismissEvent)); + let open_community_repo = + cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); v_stack() .elevation_3(cx) @@ -294,7 +296,7 @@ impl Render for FeedbackModal { .child(Button::new("community_repo", "Community Repo") .style(ButtonStyle::Filled) .color(Color::Muted) - // .on_click(cx.dispatch_action(Box::new(OpenZedCommunityRepo))) + .on_click(open_community_repo) ) .child(h_stack().justify_between().gap_1() .child( From 1abc9800affb0137b22877d06ca82a6bc0ee7d4e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 19:11:24 -0500 Subject: [PATCH 19/27] Switch to if let statement --- crates/feedback2/src/feedback_modal.rs | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 50bd75f6eb..03681ae334 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -162,24 +162,24 @@ impl FeedbackModal { 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); + + if let Err(error) = + FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await + { + 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(); }) - .log_err(); - } + .detach(); + feedback_editor.set_pending_submission(false, cx); + }) + .log_err(); } } }) From bbe9986f382a45d3edf8e34f062cf9f7b4b6ebd2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 19:13:38 -0500 Subject: [PATCH 20/27] Start work to add a confirmation when exiting feedback containing text --- crates/feedback2/src/feedback_modal.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 03681ae334..626811dff8 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -241,10 +241,21 @@ impl Render for FeedbackModal { && valid_email_address && !self.pending_submission; - let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent)); - // let open_community_issues = - // cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); - // let open_community_discussions = cx.listener(|_, _, cx| cx.emit(DismissEvent)); + let dismiss = cx.listener(|_, _, cx| { + // TODO + // if self.feedback_editor.read(cx).text_option(cx).is_some() { + // let answer = cx.prompt(PromptLevel::Info, "Exit feedback?", &["Yes", "No"]); + // cx.spawn(|_, cx| async move { + // let answer = answer.await.ok(); + // if answer == Some(0) { + // cx.emit(DismissEvent); + // } + // }) + // .detach(); + // } + + cx.emit(DismissEvent); + }); let open_community_repo = cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); From 57a9f024e7e8f14e483b8180107da8023cc8f022 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 19:26:25 -0500 Subject: [PATCH 21/27] Remove email address from local database if user removes it --- crates/feedback2/src/feedback_modal.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 626811dff8..52eca7b7f9 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -152,11 +152,18 @@ impl FeedbackModal { cx.spawn(|this, mut cx| async move { let answer = answer.await.ok(); if answer == Some(0) { - if let Some(email) = email.clone() { - let _ = KEY_VALUE_STORE - .write_kvp(DATABASE_KEY_NAME.to_string(), email) - .await; - } + match email.clone() { + Some(email) => { + let _ = KEY_VALUE_STORE + .write_kvp(DATABASE_KEY_NAME.to_string(), email) + .await; + } + None => { + let _ = KEY_VALUE_STORE + .delete_kvp(DATABASE_KEY_NAME.to_string()) + .await; + } + }; this.update(&mut cx, |feedback_editor, cx| { feedback_editor.set_pending_submission(true, cx); From 668f5accab308d67c3c687f737820a8315961861 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 19:35:41 -0500 Subject: [PATCH 22/27] Highlight whole modal as needing UI tweaks --- crates/feedback2/src/feedback_modal.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 52eca7b7f9..b24a7c59f9 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -270,6 +270,8 @@ impl Render for FeedbackModal { .elevation_3(cx) .min_w(rems(40.)) .max_w(rems(96.)) + .border() + .border_color(red()) .h(rems(40.)) .p_2() .gap_2() @@ -277,8 +279,6 @@ impl Render for FeedbackModal { v_stack().child( div() .size_full() - .border() - .border_color(red()) .child(Label::new("Give Feedback").color(Color::Default)) .child(Label::new("This editor supports markdown").color(Color::Muted)), ), @@ -292,7 +292,7 @@ impl Render for FeedbackModal { .child(self.feedback_editor.clone()), ) .child( - div().border().border_color(red()).child( + div().child( Label::new(format!( "{} / {} Characters", self.character_count, From d62b151bc02b172b2621d26a461b9ee702ab1667 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 21:23:51 -0500 Subject: [PATCH 23/27] Improve character limit bounds indicator --- crates/feedback2/src/feedback_modal.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index b24a7c59f9..282e6999e0 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -244,9 +244,16 @@ impl Render for FeedbackModal { None => true, }; - let allow_submission = FEEDBACK_CHAR_LIMIT.contains(&self.character_count) - && valid_email_address - && !self.pending_submission; + let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count); + let characters_remaining = + if valid_character_count || self.character_count > *FEEDBACK_CHAR_LIMIT.end() { + *FEEDBACK_CHAR_LIMIT.end() as i32 - self.character_count as i32 + } else { + self.character_count as i32 - *FEEDBACK_CHAR_LIMIT.start() as i32 + }; + + let allow_submission = + valid_character_count && valid_email_address && !self.pending_submission; let dismiss = cx.listener(|_, _, cx| { // TODO @@ -294,11 +301,11 @@ impl Render for FeedbackModal { .child( div().child( Label::new(format!( - "{} / {} Characters", - self.character_count, - FEEDBACK_CHAR_LIMIT.end() + "Characters: {}", + characters_remaining )) - .color(Color::Default), + .when(valid_character_count, |this| this.color(Color::Success)) + .when(!valid_character_count, |this| this.color(Color::Error)) ), ) .child( div() From 256f0308aefdd2c5b71997776c5cf0a22ac4c398 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 21:32:33 -0500 Subject: [PATCH 24/27] Renove unused code --- crates/feedback2/src/feedback_modal.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 282e6999e0..72c16319bf 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -41,7 +41,6 @@ pub struct FeedbackModal { system_specs: SystemSpecs, feedback_editor: View, email_address_editor: View, - project: Model, character_count: usize, pending_submission: bool, } @@ -132,7 +131,6 @@ impl FeedbackModal { system_specs: system_specs.clone(), feedback_editor, email_address_editor, - project, pending_submission: false, character_count: 0, } From 8fc15c05c5a729924762d14be33e6f436105c4aa Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 22:34:14 -0500 Subject: [PATCH 25/27] Introduce `when_else()` --- crates/gpui2/src/element.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 3c8f678b89..b876e375da 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -69,6 +69,24 @@ pub trait IntoElement: Sized { self.map(|this| if condition { then(this) } else { this }) } + fn when_else( + self, + condition: bool, + then: impl FnOnce(Self) -> Self, + otherwise: impl FnOnce(Self) -> Self, + ) -> Self + where + Self: Sized, + { + self.map(|this| { + if condition { + then(this) + } else { + otherwise(this) + } + }) + } + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self where Self: Sized, From ee45db8a78cbce9a6ffdb99cf3ebe4ea379821d3 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 22:34:28 -0500 Subject: [PATCH 26/27] Use `when_else()` --- crates/ui2/src/components/keybinding.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index c4054fa1a4..29586fd194 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -98,10 +98,11 @@ impl RenderOnce for Key { div() .py_0() - .when(single_char, |el| { - el.w(rems(14. / 16.)).flex().flex_none().justify_center() - }) - .when(!single_char, |el| el.px_0p5()) + .when_else( + single_char, + |el| el.w(rems(14. / 16.)).flex().flex_none().justify_center(), + |el| el.px_0p5(), + ) .h(rems(14. / 16.)) .text_ui() .line_height(relative(1.)) From 79e0d8ce3bb032cdca6a1ab28276d5480ceca5d7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 23:15:04 -0500 Subject: [PATCH 27/27] WIP --- Cargo.lock | 1 + crates/feedback2/Cargo.toml | 5 +- crates/feedback2/src/feedback2.rs | 4 +- crates/feedback2/src/feedback_modal.rs | 71 ++++++++++++++++++-------- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b156b29145..4e59f245dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3195,6 +3195,7 @@ dependencies = [ "language2", "lazy_static", "log", + "menu2", "postage", "project2", "regex", diff --git a/crates/feedback2/Cargo.toml b/crates/feedback2/Cargo.toml index 6360bd193f..560c5a307f 100644 --- a/crates/feedback2/Cargo.toml +++ b/crates/feedback2/Cargo.toml @@ -14,16 +14,17 @@ test-support = [] 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" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } project = { package = "project2", path = "../project2" } regex.workspace = true search = { package = "search2", path = "../search2" } settings = { package = "settings2", path = "../settings2" } theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } workspace = { package = "workspace2", path = "../workspace2"} -ui = { package = "ui2", path = "../ui2" } log.workspace = true futures.workspace = true diff --git a/crates/feedback2/src/feedback2.rs b/crates/feedback2/src/feedback2.rs index 63a1e86211..1a1bd93526 100644 --- a/crates/feedback2/src/feedback2.rs +++ b/crates/feedback2/src/feedback2.rs @@ -17,11 +17,11 @@ actions!( ); pub fn init(cx: &mut AppContext) { - // TODO - a way to combine these two into one? + // 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, _cx| { + cx.observe_new_views(|workspace: &mut Workspace, _| { workspace .register_action(|_, _: &CopySystemSpecsIntoClipboard, cx| { let specs = SystemSpecs::new(&cx).to_string(); diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 72c16319bf..68fbcfb3a3 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -231,6 +231,14 @@ impl FeedbackModal { } Ok(()) } + + // TODO: Escape button calls dismiss + // TODO: Should do same as hitting cancel / clicking outside of modal + // Close immediately if no text in field + // Ask to close if text in the field + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(DismissEvent); + } } impl Render for FeedbackModal { @@ -253,26 +261,35 @@ impl Render for FeedbackModal { let allow_submission = valid_character_count && valid_email_address && !self.pending_submission; - let dismiss = cx.listener(|_, _, cx| { - // TODO - // if self.feedback_editor.read(cx).text_option(cx).is_some() { - // let answer = cx.prompt(PromptLevel::Info, "Exit feedback?", &["Yes", "No"]); - // cx.spawn(|_, cx| async move { - // let answer = answer.await.ok(); - // if answer == Some(0) { - // cx.emit(DismissEvent); - // } - // }) - // .detach(); - // } + let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some(); + let submit_button_text = if self.pending_submission { + "Sending..." + } else { + "Send Feedback" + }; + let dismiss = cx.listener(|_, _, cx| { cx.emit(DismissEvent); }); + // TODO: get the "are you sure you want to dismiss?" prompt here working + let dismiss_prompt = cx.listener(|_, _, _| { + // let answer = cx.prompt(PromptLevel::Info, "Exit feedback?", &["Yes", "No"]); + // cx.spawn(|_, _| async move { + // let answer = answer.await.ok(); + // if answer == Some(0) { + // cx.emit(DismissEvent); + // } + // }) + // .detach(); + }); let open_community_repo = cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); + // TODO: Nate UI pass v_stack() .elevation_3(cx) + .key_context("GiveFeedback") + .on_action(cx.listener(Self::cancel)) .min_w(rems(40.)) .max_w(rems(96.)) .border() @@ -302,11 +319,15 @@ impl Render for FeedbackModal { "Characters: {}", characters_remaining )) - .when(valid_character_count, |this| this.color(Color::Success)) - .when(!valid_character_count, |this| this.color(Color::Error)) + .when_else( + valid_character_count, + |this| this.color(Color::Success), + |this| this.color(Color::Error) + ) ), ) - .child( div() + .child( + div() .bg(cx.theme().colors().editor_background) .border() .border_color(cx.theme().colors().border) @@ -326,14 +347,24 @@ impl Render for FeedbackModal { Button::new("cancel_feedback", "Cancel") .style(ButtonStyle::Subtle) .color(Color::Muted) - .on_click(dismiss), + // TODO: replicate this logic when clicking outside the modal + // TODO: Will require somehow overriding the modal dismal default behavior + .when_else( + has_feedback, + |this| this.on_click(dismiss_prompt), + |this| this.on_click(dismiss) + ) ) .child( - Button::new("send_feedback", "Send Feedback") + Button::new("send_feedback", submit_button_text) .color(Color::Accent) .style(ButtonStyle::Filled) - // TODO - error handling - show modal on error - .on_click(cx.listener(|this, _, cx| {let _ = this.submit(cx);})) + // TODO: Ensure that while submitting, "Sending..." is shown and disable the button + // TODO: If submit errors: show popup with error, don't close modal, set text back to "Send Feedback", and re-enable button + // TODO: If submit is successful, close the modal + .on_click(cx.listener(|this, _, cx| { + let _ = this.submit(cx); + })) .tooltip(|cx| { Tooltip::with_meta( "Submit feedback to the Zed team.", @@ -342,7 +373,7 @@ impl Render for FeedbackModal { cx, ) }) - .when(!allow_submission, |this| this.disabled(true)), + .when(!allow_submission, |this| this.disabled(true)) ), )