From 46c998ca8d311c919fcf02ff33357808633657ac Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 17:27:10 -0500 Subject: [PATCH] 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 c2efbdd863d43b4aaf8aee4a95c4c2fac085bace..b156b29145384a9b349c5f07f549d839e67649a5 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 7cf50a50e19aa6ca75897a46be00c5413fd7fbd5..cdaa42972e4ec3af3b638bd77b52192037dcbbb8 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 fbf033919dbaea03035f915b077fbc034df59018..6360bd193f4fe2f62b4b2caf4f20d80e704367e9 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 c4ab36005ae42a19b43c8671bb516f778c4cb80f..e5884cf9b185528b741cfa3b8dde4e8fe55b7812 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 19c26e4f1cf2d49c0f8154961b7f9ed69885773b..63a1e86211215a4d078cb96098dc4d044cd750bf 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 d569566ba6ba02c2d643726ba77a07cf39b30234..0000000000000000000000000000000000000000 --- 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 643e5c7b0cbf923efea47df6cb58387cb1f6c24a..0000000000000000000000000000000000000000 --- 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 3694a257102e4d04df832ff07503d625feea6558..ecbb242ed2f11b6ead44dc2b5c4bc0f62d8dccaf 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(); - // }) - // } - - // 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(); - // } - // } + 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 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), - // )) - // } + if let Some(email) = email.clone() { + cx.spawn(|_, _| KEY_VALUE_STORE.write_kvp(DATABASE_KEY_NAME.to_string(), email.clone())) + .detach() + } - // fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - // cx.emit(DismissEvent); - // } + 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 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(); - // } + fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext) { + self.pending_submission = pending_submission; + cx.notify(); + } - // 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 220a6d3406166fea3b7426fc54545b208830acfc..0000000000000000000000000000000000000000 --- 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 f3f8d3b9f4f84bb648d8cbc23a1aae98fd50d75b..b3e521850a2b38bc8824700980843d8fb8e22763 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);