Cargo.lock 🔗
@@ -3186,6 +3186,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client2",
+ "db2",
"editor2",
"futures 0.3.28",
"gpui2",
Joseph T. Lyons created
Cargo.lock | 1
crates/editor2/src/editor.rs | 11
crates/feedback2/Cargo.toml | 1
crates/feedback2/src/deploy_feedback_button.rs | 5
crates/feedback2/src/feedback2.rs | 5
crates/feedback2/src/feedback_editor.rs | 485 --------------------
crates/feedback2/src/feedback_info_text.rs | 105 ----
crates/feedback2/src/feedback_modal.rs | 209 +++++---
crates/feedback2/src/submit_feedback_button.rs | 115 ----
crates/zed2/src/zed2.rs | 4
10 files changed, 146 insertions(+), 795 deletions(-)
@@ -3186,6 +3186,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client2",
+ "db2",
"editor2",
"futures 0.3.28",
"gpui2",
@@ -8176,6 +8176,17 @@ impl Editor {
self.buffer.read(cx).read(cx).text()
}
+ pub fn text_option(&self, cx: &AppContext) -> Option<String> {
+ let text = self.buffer.read(cx).read(cx).text();
+ let text = text.trim();
+
+ if text.is_empty() {
+ return None;
+ }
+
+ Some(text.to_string())
+ }
+
pub fn set_text(&mut self, text: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
this.buffer
@@ -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" }
@@ -2,17 +2,15 @@ use gpui::{AnyElement, Render, ViewContext, WeakView};
use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
-use crate::{feedback_editor::GiveFeedback, feedback_modal::FeedbackModal};
+use crate::{feedback_modal::FeedbackModal, GiveFeedback};
pub struct DeployFeedbackButton {
- active: bool,
workspace: WeakView<Workspace>,
}
impl DeployFeedbackButton {
pub fn new(workspace: &Workspace) -> Self {
DeployFeedbackButton {
- active: false,
workspace: workspace.weak_handle(),
}
}
@@ -48,6 +46,5 @@ impl StatusItemView for DeployFeedbackButton {
_item: Option<&dyn ItemHandle>,
_cx: &mut ViewContext<Self>,
) {
- // no-op
}
}
@@ -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;
@@ -1,485 +0,0 @@
-use crate::system_specs::SystemSpecs;
-use anyhow::bail;
-use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
-use editor::{Anchor, Editor, EditorEvent};
-use futures::AsyncReadExt;
-use gpui::{
- actions, serde_json, AnyElement, AnyView, AppContext, Div, EntityId, EventEmitter,
- FocusableView, Model, PromptLevel, Task, View, ViewContext, WindowContext,
-};
-use isahc::Request;
-use language::{Buffer, Event};
-use project::{search::SearchQuery, Project};
-use regex::Regex;
-use serde::Serialize;
-use std::{
- any::TypeId,
- ops::{Range, RangeInclusive},
- sync::Arc,
-};
-use ui::{prelude::*, Icon, IconElement, Label};
-use util::ResultExt;
-use workspace::{
- item::{Item, ItemEvent, ItemHandle},
- searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
- Workspace,
-};
-
-const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
-const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
- "Feedback failed to submit, see error log for details.";
-
-actions!(GiveFeedback, SubmitFeedback);
-
-pub fn init(cx: &mut AppContext) {
- cx.observe_new_views(|workspace: &mut Workspace, _| {
- workspace.register_action(|workspace, _: &GiveFeedback, cx| {
- FeedbackEditor::deploy(workspace, cx);
- });
- })
- .detach();
-}
-
-#[derive(Serialize)]
-struct FeedbackRequestBody<'a> {
- feedback_text: &'a str,
- email: Option<String>,
- metrics_id: Option<Arc<str>>,
- installation_id: Option<Arc<str>>,
- system_specs: SystemSpecs,
- is_staff: bool,
- token: &'a str,
-}
-
-#[derive(Clone)]
-pub(crate) struct FeedbackEditor {
- system_specs: SystemSpecs,
- editor: View<Editor>,
- project: Model<Project>,
- pub allow_submission: bool,
-}
-
-impl EventEmitter<Event> for FeedbackEditor {}
-impl EventEmitter<EditorEvent> for FeedbackEditor {}
-
-impl FeedbackEditor {
- fn new(
- system_specs: SystemSpecs,
- project: Model<Project>,
- buffer: Model<Buffer>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- let editor = cx.build_view(|cx| {
- let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
- editor.set_vertical_scroll_margin(5, cx);
- editor
- });
-
- cx.subscribe(
- &editor,
- |&mut _, _, e: &EditorEvent, cx: &mut ViewContext<_>| cx.emit(e.clone()),
- )
- .detach();
-
- Self {
- system_specs: system_specs.clone(),
- editor,
- project,
- allow_submission: true,
- }
- }
-
- pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
- if !self.allow_submission {
- return Task::ready(Ok(()));
- }
-
- let feedback_text = self.editor.read(cx).text(cx);
- let feedback_char_count = feedback_text.chars().count();
- let feedback_text = feedback_text.trim().to_string();
-
- let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
- Some(format!(
- "Feedback can't be shorter than {} characters.",
- FEEDBACK_CHAR_LIMIT.start()
- ))
- } else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() {
- Some(format!(
- "Feedback can't be longer than {} characters.",
- FEEDBACK_CHAR_LIMIT.end()
- ))
- } else {
- None
- };
-
- if let Some(error) = error {
- let prompt = cx.prompt(PromptLevel::Critical, &error, &["OK"]);
- cx.spawn(|_, _cx| async move {
- prompt.await.ok();
- })
- .detach();
- return Task::ready(Ok(()));
- }
-
- let answer = cx.prompt(
- PromptLevel::Info,
- "Ready to submit your feedback?",
- &["Yes, Submit!", "No"],
- );
-
- let client = cx.global::<Arc<Client>>().clone();
- let specs = self.system_specs.clone();
-
- cx.spawn(|this, mut cx| async move {
- let answer = answer.await.ok();
-
- if answer == Some(0) {
- this.update(&mut cx, |feedback_editor, cx| {
- feedback_editor.set_allow_submission(false, cx);
- })
- .log_err();
-
- match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
- Ok(_) => {
- this.update(&mut cx, |_, cx| cx.emit(Event::Closed))
- .log_err();
- }
-
- Err(error) => {
- log::error!("{}", error);
- this.update(&mut cx, |feedback_editor, cx| {
- let prompt = cx.prompt(
- PromptLevel::Critical,
- FEEDBACK_SUBMISSION_ERROR_TEXT,
- &["OK"],
- );
- cx.spawn(|_, _cx| async move {
- prompt.await.ok();
- })
- .detach();
- feedback_editor.set_allow_submission(true, cx);
- })
- .log_err();
- }
- }
- }
- })
- .detach();
-
- Task::ready(Ok(()))
- }
-
- fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext<Self>) {
- self.allow_submission = allow_submission;
- cx.notify();
- }
-
- async fn submit_feedback(
- feedback_text: &str,
- zed_client: Arc<Client>,
- system_specs: SystemSpecs,
- ) -> anyhow::Result<()> {
- let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
-
- let telemetry = zed_client.telemetry();
- let metrics_id = telemetry.metrics_id();
- let installation_id = telemetry.installation_id();
- let is_staff = telemetry.is_staff();
- let http_client = zed_client.http_client();
-
- let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
-
- let emails: Vec<&str> = re
- .captures_iter(feedback_text)
- .map(|capture| capture.get(0).unwrap().as_str())
- .collect();
-
- let email = emails.first().map(|e| e.to_string());
-
- let request = FeedbackRequestBody {
- feedback_text: &feedback_text,
- email,
- metrics_id,
- installation_id,
- system_specs,
- is_staff: is_staff.unwrap_or(false),
- token: ZED_SECRET_CLIENT_TOKEN,
- };
-
- let json_bytes = serde_json::to_vec(&request)?;
-
- let request = Request::post(feedback_endpoint)
- .header("content-type", "application/json")
- .body(json_bytes.into())?;
-
- let mut response = http_client.send(request).await?;
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
-
- let response_status = response.status();
-
- if !response_status.is_success() {
- bail!("Feedback API failed with error: {}", response_status)
- }
-
- Ok(())
- }
-}
-
-impl FeedbackEditor {
- pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
- let markdown = workspace
- .app_state()
- .languages
- .language_for_name("Markdown");
- cx.spawn(|workspace, mut cx| async move {
- let markdown = markdown.await.log_err();
- workspace
- .update(&mut cx, |workspace, cx| {
- workspace.with_local_workspace(cx, |workspace, cx| {
- let project = workspace.project().clone();
- let buffer = project
- .update(cx, |project, cx| project.create_buffer("", markdown, cx))
- .expect("creating buffers on a local workspace always succeeds");
- let system_specs = SystemSpecs::new(cx);
- let feedback_editor = cx.build_view(|cx| {
- FeedbackEditor::new(system_specs, project, buffer, cx)
- });
- workspace.add_item(Box::new(feedback_editor), cx);
- })
- })?
- .await
- })
- .detach_and_log_err(cx);
- }
-}
-
-// TODO
-impl Render for FeedbackEditor {
- type Element = Div;
-
- fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
- div().size_full().child(self.editor.clone())
- }
-}
-
-impl EventEmitter<ItemEvent> for FeedbackEditor {}
-
-impl FocusableView for FeedbackEditor {
- fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
- self.editor.focus_handle(cx)
- }
-}
-
-impl Item for FeedbackEditor {
- fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
- Some("Send Feedback".into())
- }
-
- fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
- h_stack()
- .gap_1()
- .child(IconElement::new(Icon::Envelope).color(Color::Accent))
- .child(Label::new("Send Feedback".to_string()))
- .into_any_element()
- }
-
- fn for_each_project_item(
- &self,
- cx: &AppContext,
- f: &mut dyn FnMut(EntityId, &dyn project::Item),
- ) {
- self.editor.for_each_project_item(cx, f)
- }
-
- fn is_singleton(&self, _: &AppContext) -> bool {
- true
- }
-
- fn can_save(&self, _: &AppContext) -> bool {
- true
- }
-
- fn save(
- &mut self,
- _project: Model<Project>,
- cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- self.submit(cx)
- }
-
- fn save_as(
- &mut self,
- _: Model<Project>,
- _: std::path::PathBuf,
- cx: &mut ViewContext<Self>,
- ) -> Task<anyhow::Result<()>> {
- self.submit(cx)
- }
-
- fn reload(&mut self, _: Model<Project>, _: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
- Task::Ready(Some(Ok(())))
- }
-
- fn clone_on_split(
- &self,
- _workspace_id: workspace::WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Option<View<Self>>
- where
- Self: Sized,
- {
- let buffer = self
- .editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .expect("Feedback buffer is only ever singleton");
-
- Some(cx.build_view(|cx| {
- Self::new(
- self.system_specs.clone(),
- self.project.clone(),
- buffer.clone(),
- cx,
- )
- }))
- }
-
- fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
- Some(Box::new(handle.clone()))
- }
-
- fn act_as_type<'a>(
- &'a self,
- type_id: TypeId,
- self_handle: &'a View<Self>,
- cx: &'a AppContext,
- ) -> Option<AnyView> {
- if type_id == TypeId::of::<Self>() {
- Some(self_handle.to_any())
- } else if type_id == TypeId::of::<Editor>() {
- Some(self.editor.to_any())
- } else {
- None
- }
- }
-
- fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
-
- fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
-
- fn navigate(&mut self, _: Box<dyn std::any::Any>, _: &mut ViewContext<Self>) -> bool {
- false
- }
-
- fn tab_description(&self, _: usize, _: &AppContext) -> Option<ui::prelude::SharedString> {
- None
- }
-
- fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
-
- fn is_dirty(&self, _: &AppContext) -> bool {
- false
- }
-
- fn has_conflict(&self, _: &AppContext) -> bool {
- false
- }
-
- fn breadcrumb_location(&self) -> workspace::ToolbarItemLocation {
- workspace::ToolbarItemLocation::Hidden
- }
-
- fn breadcrumbs(
- &self,
- _theme: &theme::Theme,
- _cx: &AppContext,
- ) -> Option<Vec<workspace::item::BreadcrumbText>> {
- None
- }
-
- fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
-
- fn serialized_item_kind() -> Option<&'static str> {
- Some("feedback")
- }
-
- fn deserialize(
- _project: gpui::Model<Project>,
- _workspace: gpui::WeakView<Workspace>,
- _workspace_id: workspace::WorkspaceId,
- _item_id: workspace::ItemId,
- _cx: &mut ViewContext<workspace::Pane>,
- ) -> Task<anyhow::Result<View<Self>>> {
- unimplemented!(
- "deserialize() must be implemented if serialized_item_kind() returns Some(_)"
- )
- }
-
- fn show_toolbar(&self) -> bool {
- true
- }
-
- fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<gpui::Point<gpui::Pixels>> {
- None
- }
-}
-
-impl EventEmitter<SearchEvent> for FeedbackEditor {}
-
-impl SearchableItem for FeedbackEditor {
- type Match = Range<Anchor>;
-
- fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| editor.clear_matches(cx))
- }
-
- fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |editor, cx| editor.update_matches(matches, cx))
- }
-
- fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
- self.editor
- .update(cx, |editor, cx| editor.query_suggestion(cx))
- }
-
- fn activate_match(
- &mut self,
- index: usize,
- matches: Vec<Self::Match>,
- cx: &mut ViewContext<Self>,
- ) {
- self.editor
- .update(cx, |editor, cx| editor.activate_match(index, matches, cx))
- }
-
- fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |e, cx| e.select_matches(matches, cx))
- }
- fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext<Self>) {
- self.editor
- .update(cx, |e, cx| e.replace(matches, query, cx));
- }
- fn find_matches(
- &mut self,
- query: Arc<project::search::SearchQuery>,
- cx: &mut ViewContext<Self>,
- ) -> Task<Vec<Self::Match>> {
- self.editor
- .update(cx, |editor, cx| editor.find_matches(query, cx))
- }
-
- fn active_match_index(
- &mut self,
- matches: Vec<Self::Match>,
- cx: &mut ViewContext<Self>,
- ) -> Option<usize> {
- self.editor
- .update(cx, |editor, cx| editor.active_match_index(matches, cx))
- }
-}
@@ -1,105 +0,0 @@
-use gpui::{Div, EventEmitter, View, ViewContext};
-use ui::{prelude::*, Label};
-use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
-
-use crate::feedback_editor::FeedbackEditor;
-
-pub struct FeedbackInfoText {
- active_item: Option<View<FeedbackEditor>>,
-}
-
-impl FeedbackInfoText {
- pub fn new() -> Self {
- Self {
- active_item: Default::default(),
- }
- }
-}
-
-// TODO
-impl Render for FeedbackInfoText {
- type Element = Div;
-
- fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
- // TODO - get this into the toolbar area like before - ensure things work the same when horizontally shrinking app
- div()
- .size_full()
- .child(Label::new("Share your feedback. Include your email for replies. For issues and discussions, visit the ").color(Color::Muted))
- .child(Label::new("community repo").color(Color::Muted)) // TODO - this needs to be a link
- .child(Label::new(".").color(Color::Muted))
- }
-}
-
-// TODO - delete
-// impl View for FeedbackInfoText {
-// fn ui_name() -> &'static str {
-// "FeedbackInfoText"
-// }
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// let theme = theme::current(cx).clone();
-
-// Flex::row()
-// .with_child(
-// Text::new(
-// "Share your feedback. Include your email for replies. For issues and discussions, visit the ",
-// theme.feedback.info_text_default.text.clone(),
-// )
-// .with_soft_wrap(false)
-// .aligned(),
-// )
-// .with_child(
-// MouseEventHandler::new::<OpenZedCommunityRepo, _>(0, cx, |state, _| {
-// let style = if state.hovered() {
-// &theme.feedback.link_text_hover
-// } else {
-// &theme.feedback.link_text_default
-// };
-// Label::new("community repo", style.text.clone())
-// .contained()
-// .with_style(style.container)
-// .aligned()
-// .left()
-// .clipped()
-// })
-// .with_cursor_style(CursorStyle::PointingHand)
-// .on_click(MouseButton::Left, |_, _, cx| {
-// open_zed_community_repo(&Default::default(), cx)
-// }),
-// )
-// .with_child(
-// Text::new(".", theme.feedback.info_text_default.text.clone())
-// .with_soft_wrap(false)
-// .aligned(),
-// )
-// .contained()
-// .with_style(theme.feedback.info_text_default.container)
-// .aligned()
-// .left()
-// .clipped()
-// .into_any()
-// }
-// }
-
-impl EventEmitter<ToolbarItemEvent> for FeedbackInfoText {}
-
-impl ToolbarItemView for FeedbackInfoText {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> workspace::ToolbarItemLocation {
- cx.notify();
-
- if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
- {
- dbg!("Editor");
- self.active_item = Some(feedback_editor);
- ToolbarItemLocation::PrimaryLeft
- } else {
- dbg!("no editor");
- self.active_item = None;
- ToolbarItemLocation::Hidden
- }
- }
-}
@@ -1,29 +1,50 @@
-use std::ops::RangeInclusive;
+use std::{ops::RangeInclusive, sync::Arc};
+use anyhow::bail;
+use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent};
+use futures::AsyncReadExt;
use gpui::{
- div, red, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
- Render, View, ViewContext,
+ div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+ FocusableView, Model, PromptLevel, Render, Task, View, ViewContext,
};
+use isahc::Request;
use language::Buffer;
use project::Project;
+use regex::Regex;
+use serde_derive::Serialize;
use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip};
use util::ResultExt;
-use workspace::{item::Item, Workspace};
+use workspace::Workspace;
-use crate::{feedback_editor::GiveFeedback, system_specs::SystemSpecs, OpenZedCommunityRepo};
+use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
+const DATABASE_KEY_NAME: &str = "email_address";
+const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
+#[derive(Serialize)]
+struct FeedbackRequestBody<'a> {
+ feedback_text: &'a str,
+ email: Option<String>,
+ metrics_id: Option<Arc<str>>,
+ installation_id: Option<Arc<str>>,
+ system_specs: SystemSpecs,
+ is_staff: bool,
+ token: &'a str,
+}
+
pub struct FeedbackModal {
system_specs: SystemSpecs,
feedback_editor: View<Editor>,
email_address_editor: View<Editor>,
project: Model<Project>,
- pub allow_submission: bool,
character_count: usize,
+ allow_submission: bool,
+ pub pending_submission: bool,
}
impl FocusableView for FeedbackModal {
@@ -75,8 +96,14 @@ impl FeedbackModal {
let email_address_editor = cx.build_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("Email address (optional)", cx);
+
+ if let Ok(Some(email_address)) = KEY_VALUE_STORE.read_kvp(DATABASE_KEY_NAME) {
+ editor.set_text(email_address, cx)
+ }
+
editor
});
+
let feedback_editor = cx.build_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_vertical_scroll_margin(5, cx);
@@ -107,92 +134,116 @@ impl FeedbackModal {
feedback_editor,
email_address_editor,
project,
- allow_submission: true,
+ allow_submission: false,
+ pending_submission: false,
character_count: 0,
}
}
- // fn release(&mut self, cx: &mut WindowContext) {
- // let scroll_position = self.prev_scroll_position.take();
- // self.active_editor.update(cx, |editor, cx| {
- // editor.highlight_rows(None);
- // if let Some(scroll_position) = scroll_position {
- // editor.set_scroll_position(scroll_position, cx);
- // }
- // cx.notify();
- // })
- // }
-
- // fn on_feedback_editor_event(
- // &mut self,
- // _: View<Editor>,
- // event: &editor::EditorEvent,
- // cx: &mut ViewContext<Self>,
- // ) {
- // match event {
- // // todo!() this isn't working...
- // editor::EditorEvent::Blurred => cx.emit(DismissEvent),
- // editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
- // _ => {}
- // }
- // }
-
- // fn highlight_current_line(&mut self, cx: &mut ViewContext<Self>) {
- // if let Some(point) = self.point_from_query(cx) {
- // self.active_editor.update(cx, |active_editor, cx| {
- // let snapshot = active_editor.snapshot(cx).display_snapshot;
- // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
- // let display_point = point.to_display_point(&snapshot);
- // let row = display_point.row();
- // active_editor.highlight_rows(Some(row..row + 1));
- // active_editor.request_autoscroll(Autoscroll::center(), cx);
- // });
- // cx.notify();
- // }
- // }
+ pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
+ if !self.allow_submission {
+ return Task::ready(Ok(()));
+ }
+ let feedback_text = self.feedback_editor.read(cx).text(cx).trim().to_string();
+ let email = self.email_address_editor.read(cx).text_option(cx);
- // fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
- // let line_editor = self.line_editor.read(cx).text(cx);
- // let mut components = line_editor
- // .splitn(2, FILE_ROW_COLUMN_DELIMITER)
- // .map(str::trim)
- // .fuse();
- // let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
- // let column = components.next().and_then(|col| col.parse::<u32>().ok());
- // Some(Point::new(
- // row.saturating_sub(1),
- // column.unwrap_or(0).saturating_sub(1),
- // ))
- // }
+ if let Some(email) = email.clone() {
+ cx.spawn(|_, _| KEY_VALUE_STORE.write_kvp(DATABASE_KEY_NAME.to_string(), email.clone()))
+ .detach()
+ }
- // fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- // cx.emit(DismissEvent);
- // }
+ let answer = cx.prompt(
+ PromptLevel::Info,
+ "Ready to submit your feedback?",
+ &["Yes, Submit!", "No"],
+ );
+ let client = cx.global::<Arc<Client>>().clone();
+ let specs = self.system_specs.clone();
+ cx.spawn(|this, mut cx| async move {
+ let answer = answer.await.ok();
+ if answer == Some(0) {
+ this.update(&mut cx, |feedback_editor, cx| {
+ feedback_editor.set_pending_submission(true, cx);
+ })
+ .log_err();
+ match FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await {
+ Ok(_) => {}
+ Err(error) => {
+ log::error!("{}", error);
+ this.update(&mut cx, |feedback_editor, cx| {
+ let prompt = cx.prompt(
+ PromptLevel::Critical,
+ FEEDBACK_SUBMISSION_ERROR_TEXT,
+ &["OK"],
+ );
+ cx.spawn(|_, _cx| async move {
+ prompt.await.ok();
+ })
+ .detach();
+ feedback_editor.set_pending_submission(false, cx);
+ })
+ .log_err();
+ }
+ }
+ }
+ })
+ .detach();
+ Task::ready(Ok(()))
+ }
- // fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
- // if let Some(point) = self.point_from_query(cx) {
- // self.active_editor.update(cx, |editor, cx| {
- // let snapshot = editor.snapshot(cx).display_snapshot;
- // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
- // editor.change_selections(Some(Autoscroll::center()), cx, |s| {
- // s.select_ranges([point..point])
- // });
- // editor.focus(cx);
- // cx.notify();
- // });
- // self.prev_scroll_position.take();
- // }
+ fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext<Self>) {
+ self.pending_submission = pending_submission;
+ cx.notify();
+ }
- // cx.emit(DismissEvent);
- // }
+ async fn submit_feedback(
+ feedback_text: &str,
+ email: Option<String>,
+ zed_client: Arc<Client>,
+ system_specs: SystemSpecs,
+ ) -> anyhow::Result<()> {
+ let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
+ let telemetry = zed_client.telemetry();
+ let metrics_id = telemetry.metrics_id();
+ let installation_id = telemetry.installation_id();
+ let is_staff = telemetry.is_staff();
+ let http_client = zed_client.http_client();
+ let request = FeedbackRequestBody {
+ feedback_text: &feedback_text,
+ email,
+ metrics_id,
+ installation_id,
+ system_specs,
+ is_staff: is_staff.unwrap_or(false),
+ token: ZED_SECRET_CLIENT_TOKEN,
+ };
+ let json_bytes = serde_json::to_vec(&request)?;
+ let request = Request::post(feedback_endpoint)
+ .header("content-type", "application/json")
+ .body(json_bytes.into())?;
+ let mut response = http_client.send(request).await?;
+ let mut body = String::new();
+ response.body_mut().read_to_string(&mut body).await?;
+ let response_status = response.status();
+ if !response_status.is_success() {
+ bail!("Feedback API failed with error: {}", response_status)
+ }
+ Ok(())
+ }
}
impl Render for FeedbackModal {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let character_count_error = (self.character_count < *FEEDBACK_CHAR_LIMIT.start())
- || (self.character_count > *FEEDBACK_CHAR_LIMIT.end());
+ let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
+ Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
+ None => true,
+ };
+
+ self.allow_submission = FEEDBACK_CHAR_LIMIT.contains(&self.character_count)
+ && valid_email_address
+ && !self.pending_submission;
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
// let open_community_issues =
@@ -268,7 +319,7 @@ impl Render for FeedbackModal {
cx,
)
})
- .when(character_count_error, |this| this.disabled(true)),
+ .when(!self.allow_submission, |this| this.disabled(true)),
),
)
@@ -1,115 +0,0 @@
-use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
-use anyhow::Result;
-use gpui::{AppContext, Div, EventEmitter, Render, Task, View, ViewContext};
-use ui::prelude::*;
-use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
-
-pub fn init(cx: &mut AppContext) {
- // cx.add_action(SubmitFeedbackButton::submit);
-}
-
-pub struct SubmitFeedbackButton {
- pub(crate) active_item: Option<View<FeedbackEditor>>,
-}
-
-impl SubmitFeedbackButton {
- pub fn new() -> Self {
- Self {
- active_item: Default::default(),
- }
- }
-
- pub fn submit(
- &mut self,
- _: &SubmitFeedback,
- cx: &mut ViewContext<Self>,
- ) -> Option<Task<Result<()>>> {
- if let Some(active_item) = self.active_item.as_ref() {
- Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.submit(cx)))
- } else {
- None
- }
- }
-}
-
-// TODO
-impl Render for SubmitFeedbackButton {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let allow_submission = self
- .active_item
- .as_ref()
- .map_or(true, |i| i.read(cx).allow_submission);
-
- div()
- }
-}
-
-// TODO - delete
-// impl View for SubmitFeedbackButton {
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// let theme = theme::current(cx).clone();
-// let allow_submission = self
-// .active_item
-// .as_ref()
-// .map_or(true, |i| i.read(cx).allow_submission);
-
-// enum SubmitFeedbackButton {}
-// MouseEventHandler::new::<SubmitFeedbackButton, _>(0, cx, |state, _| {
-// let text;
-// let style = if allow_submission {
-// text = "Submit as Markdown";
-// theme.feedback.submit_button.style_for(state)
-// } else {
-// text = "Submitting...";
-// theme
-// .feedback
-// .submit_button
-// .disabled
-// .as_ref()
-// .unwrap_or(&theme.feedback.submit_button.default)
-// };
-
-// Label::new(text, style.text.clone())
-// .contained()
-// .with_style(style.container)
-// })
-// .with_cursor_style(CursorStyle::PointingHand)
-// .on_click(MouseButton::Left, |_, this, cx| {
-// this.submit(&Default::default(), cx);
-// })
-// .aligned()
-// .contained()
-// .with_margin_left(theme.feedback.button_margin)
-// .with_tooltip::<Self>(
-// 0,
-// "cmd-s",
-// Some(Box::new(SubmitFeedback)),
-// theme.tooltip.clone(),
-// cx,
-// )
-// .into_any()
-// }
-// }
-
-impl EventEmitter<ToolbarItemEvent> for SubmitFeedbackButton {}
-
-impl ToolbarItemView for SubmitFeedbackButton {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- cx: &mut ViewContext<Self>,
- ) -> workspace::ToolbarItemLocation {
- cx.notify();
- if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
- {
- self.active_item = Some(feedback_editor);
- ToolbarItemLocation::PrimaryRight
- } else {
- self.active_item = None;
- ToolbarItemLocation::Hidden
- }
- }
-}
@@ -10,7 +10,6 @@ pub use assets::*;
use breadcrumbs::Breadcrumbs;
use collections::VecDeque;
use editor::{Editor, MultiBuffer};
-use feedback::submit_feedback_button::SubmitFeedbackButton;
use gpui::{
actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions,
ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions,
@@ -111,9 +110,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// toolbar.add_item(diagnostic_editor_controls, cx);
// let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
// toolbar.add_item(project_search_bar, cx);
- let submit_feedback_button =
- cx.build_view(|_| SubmitFeedbackButton::new());
- toolbar.add_item(submit_feedback_button, cx);
// let lsp_log_item =
// cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
// toolbar.add_item(lsp_log_item, cx);