This commit is contained in:
Joseph T. Lyons 2023-12-06 17:27:10 -05:00
parent f4c7b13397
commit 46c998ca8d
10 changed files with 146 additions and 795 deletions

1
Cargo.lock generated
View File

@ -3186,6 +3186,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"db2",
"editor2",
"futures 0.3.28",
"gpui2",

View File

@ -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

View File

@ -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" }

View File

@ -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
}
}

View File

@ -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;

View File

@ -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))
}
}

View File

@ -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
}
}
}

View File

@ -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();
// })
// }
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 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),
// _ => {}
// }
// }
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<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();
// }
// }
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 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),
// ))
// }
fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext<Self>) {
self.pending_submission = pending_submission;
cx.notify();
}
// fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
// cx.emit(DismissEvent);
// }
// 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();
// }
// 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)),
),
)

View File

@ -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
}
}
}

View File

@ -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);