Complete first iteration of in-app feedback

This commit is contained in:
Joseph Lyons 2023-01-23 00:59:46 -05:00
parent 083986dfae
commit fb2278dc6d
5 changed files with 113 additions and 130 deletions

1
Cargo.lock generated
View file

@ -2035,6 +2035,7 @@ dependencies = [
"isahc", "isahc",
"language", "language",
"lazy_static", "lazy_static",
"log",
"postage", "postage",
"project", "project",
"search", "search",

View file

@ -14,6 +14,7 @@ anyhow = "1.0.38"
client = { path = "../client" } client = { path = "../client" }
editor = { path = "../editor" } editor = { path = "../editor" }
language = { path = "../language" } language = { path = "../language" }
log = "0.4"
futures = "0.3" futures = "0.3"
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
human_bytes = "0.4.1" human_bytes = "0.4.1"

View file

@ -7,10 +7,9 @@ use serde::Deserialize;
use system_specs::SystemSpecs; use system_specs::SystemSpecs;
use workspace::Workspace; use workspace::Workspace;
// TODO FEEDBACK: This open brownser code is duplicated from the zed crate, where should we refactor it to?
#[derive(Deserialize, Clone, PartialEq)] #[derive(Deserialize, Clone, PartialEq)]
struct OpenBrowser { pub struct OpenBrowser {
url: Arc<str>, pub url: Arc<str>,
} }
impl_actions!(zed, [OpenBrowser]); impl_actions!(zed, [OpenBrowser]);

View file

@ -2,7 +2,7 @@ use std::{ops::Range, sync::Arc};
use anyhow::bail; use anyhow::bail;
use client::{Client, ZED_SECRET_CLIENT_TOKEN}; use client::{Client, ZED_SECRET_CLIENT_TOKEN};
use editor::Editor; use editor::{Anchor, Editor};
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{ use gpui::{
actions, actions,
@ -21,6 +21,7 @@ use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use workspace::{ use workspace::{
item::{Item, ItemHandle}, item::{Item, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle},
StatusItemView, Workspace, StatusItemView, Workspace,
}; };
@ -31,13 +32,15 @@ lazy_static! {
std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string()); std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string());
} }
// TODO FEEDBACK: In the future, it would be nice to use this is some sort of live-rendering character counter thing
// Currently, we are just checking on submit that the the text exceeds the `start` value in this range
const FEEDBACK_CHAR_COUNT_RANGE: Range<usize> = Range { const FEEDBACK_CHAR_COUNT_RANGE: Range<usize> = Range {
start: 5, start: 10,
end: 1000, end: 1000,
}; };
const FEEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here in the form of Markdown. Save the tab to submit your feedback.";
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]); actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
@ -170,26 +173,21 @@ impl FeedbackEditor {
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
const FEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here in the form of Markdown. Save the tab to submit your feedback.";
let editor = cx.add_view(|cx| { let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_vertical_scroll_margin(5, cx); editor.set_vertical_scroll_margin(5, cx);
editor.set_placeholder_text(FEDBACK_PLACEHOLDER_TEXT, cx); editor.set_placeholder_text(FEEDBACK_PLACEHOLDER_TEXT, cx);
editor editor
}); });
cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()))
.detach();
let this = Self { editor, project }; let this = Self { editor, project };
this this
} }
fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self { fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
// TODO FEEDBACK: This doesn't work like I expected it would
// let markdown_language = Arc::new(Language::new(
// LanguageConfig::default(),
// Some(tree_sitter_markdown::language()),
// ));
let markdown_language = project.read(cx).languages().get_language("Markdown"); let markdown_language = project.read(cx).languages().get_language("Markdown");
let buffer = project let buffer = project
@ -206,7 +204,6 @@ impl FeedbackEditor {
_: gpui::ModelHandle<Project>, _: gpui::ModelHandle<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
// TODO FEEDBACK: These don't look right
let feedback_text_length = self.editor.read(cx).buffer().read(cx).len(cx); let feedback_text_length = self.editor.read(cx).buffer().read(cx).len(cx);
if feedback_text_length <= FEEDBACK_CHAR_COUNT_RANGE.start { if feedback_text_length <= FEEDBACK_CHAR_COUNT_RANGE.start {
@ -223,35 +220,42 @@ impl FeedbackEditor {
} }
let mut answer = cx.prompt( let mut answer = cx.prompt(
PromptLevel::Warning, PromptLevel::Info,
"Ready to submit your feedback?", "Ready to submit your feedback?",
&["Yes, Submit!", "No"], &["Yes, Submit!", "No"],
); );
let this = cx.handle(); let this = cx.handle();
let client = cx.global::<Arc<Client>>().clone();
let feedback_text = self.editor.read(cx).text(cx);
let specs = SystemSpecs::new(cx);
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
let answer = answer.recv().await; let answer = answer.recv().await;
if answer == Some(0) { if answer == Some(0) {
cx.update(|cx| { match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
this.update(cx, |this, cx| match this.submit_feedback(cx) { Ok(_) => {
// TODO FEEDBACK cx.update(|cx| {
Ok(_) => { this.update(cx, |_, cx| {
// Close file after feedback sent successfully cx.dispatch_action(workspace::CloseActiveItem);
// workspace })
// .update(cx, |workspace, cx| { });
// Pane::close_active_item(workspace, &Default::default(), cx) }
// .unwrap() Err(error) => {
// }) log::error!("{}", error);
// .await
// .unwrap(); cx.update(|cx| {
} this.update(cx, |_, cx| {
Err(error) => { cx.prompt(
cx.prompt(PromptLevel::Critical, &error.to_string(), &["OK"]); PromptLevel::Critical,
// Prompt that something failed (and to check the log for the exact error? and to try again?) FEEDBACK_SUBMISSION_ERROR_TEXT,
} &["OK"],
}) );
}) })
});
}
}
} }
}) })
.detach(); .detach();
@ -259,63 +263,38 @@ impl FeedbackEditor {
Task::ready(Ok(())) Task::ready(Ok(()))
} }
fn submit_feedback(&mut self, cx: &mut ViewContext<'_, Self>) -> anyhow::Result<()> { async fn submit_feedback(
let feedback_text = self.editor.read(cx).text(cx); feedback_text: &str,
let zed_client = cx.global::<Arc<Client>>(); zed_client: Arc<Client>,
let system_specs = SystemSpecs::new(cx); system_specs: SystemSpecs,
) -> anyhow::Result<()> {
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL); let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let metrics_id = zed_client.metrics_id(); let metrics_id = zed_client.metrics_id();
let http_client = zed_client.http_client(); let http_client = zed_client.http_client();
// TODO FEEDBACK: how to get error out of the thread let request = FeedbackRequestBody {
feedback_text: &feedback_text,
metrics_id,
system_specs,
token: ZED_SECRET_CLIENT_TOKEN,
};
let this = cx.handle(); let json_bytes = serde_json::to_vec(&request)?;
cx.spawn(|_, async_cx| { let request = Request::post(feedback_endpoint)
async move { .header("content-type", "application/json")
let request = FeedbackRequestBody { .body(json_bytes.into())?;
feedback_text: &feedback_text,
metrics_id,
system_specs,
token: ZED_SECRET_CLIENT_TOKEN,
};
let json_bytes = serde_json::to_vec(&request)?; let mut response = http_client.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
let request = Request::post(feedback_endpoint) let response_status = response.status();
.header("content-type", "application/json")
.body(json_bytes.into())?;
let mut response = http_client.send(request).await?; if !response_status.is_success() {
let mut body = String::new(); bail!("Feedback API failed with error: {}", response_status)
response.body_mut().read_to_string(&mut body).await?; }
let response_status = response.status();
if !response_status.is_success() {
bail!("Feedback API failed with: {}", response_status)
}
this.read_with(&async_cx, |_this, _cx| -> anyhow::Result<()> {
bail!("Error")
})?;
// TODO FEEDBACK: Use or remove
// Will need to handle error cases
// async_cx.update(|cx| {
// this.update(cx, |this, cx| {
// this.handle_error(error);
// cx.notify();
// cx.dispatch_action(ShowErrorPopover);
// this.error_text = "Embedding failed"
// })
// });
Ok(())
}
})
.detach();
Ok(()) Ok(())
} }
@ -323,13 +302,9 @@ impl FeedbackEditor {
impl FeedbackEditor { impl FeedbackEditor {
pub fn deploy(workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>) { pub fn deploy(workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>) {
// if let Some(existing) = workspace.item_of_type::<FeedbackEditor>(cx) {
// workspace.activate_item(&existing, cx);
// } else {
let feedback_editor = let feedback_editor =
cx.add_view(|cx| FeedbackEditor::new(workspace.project().clone(), cx)); cx.add_view(|cx| FeedbackEditor::new(workspace.project().clone(), cx));
workspace.add_item(Box::new(feedback_editor), cx); workspace.add_item(Box::new(feedback_editor), cx);
// }
} }
} }
@ -350,7 +325,7 @@ impl View for FeedbackEditor {
} }
impl Entity for FeedbackEditor { impl Entity for FeedbackEditor {
type Event = (); type Event = editor::Event;
} }
impl Item for FeedbackEditor { impl Item for FeedbackEditor {
@ -453,52 +428,59 @@ impl Item for FeedbackEditor {
) -> Task<anyhow::Result<ViewHandle<Self>>> { ) -> Task<anyhow::Result<ViewHandle<Self>>> {
unreachable!() unreachable!()
} }
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
} }
// impl SearchableItem for FeedbackEditor { impl SearchableItem for FeedbackEditor {
// type Match = <Editor as SearchableItem>::Match; type Match = Range<Anchor>;
// fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> { fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
// Editor::to_search_event(event) Editor::to_search_event(event)
// } }
// fn clear_matches(&mut self, cx: &mut ViewContext<Self>) { fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
// self. self.editor
// } .update(cx, |editor, cx| editor.clear_matches(cx))
}
// fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) { fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
// todo!() self.editor
// } .update(cx, |editor, cx| editor.update_matches(matches, cx))
}
// fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String { fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
// todo!() self.editor
// } .update(cx, |editor, cx| editor.query_suggestion(cx))
}
// fn activate_match( fn activate_match(
// &mut self, &mut self,
// index: usize, index: usize,
// matches: Vec<Self::Match>, matches: Vec<Self::Match>,
// cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
// ) { ) {
// todo!() self.editor
// } .update(cx, |editor, cx| editor.activate_match(index, matches, cx))
}
// fn find_matches( fn find_matches(
// &mut self, &mut self,
// query: project::search::SearchQuery, query: project::search::SearchQuery,
// cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
// ) -> Task<Vec<Self::Match>> { ) -> Task<Vec<Self::Match>> {
// todo!() self.editor
// } .update(cx, |editor, cx| editor.find_matches(query, cx))
}
// fn active_match_index( fn active_match_index(
// &mut self, &mut self,
// matches: Vec<Self::Match>, matches: Vec<Self::Match>,
// cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
// ) -> Option<usize> { ) -> Option<usize> {
// todo!() self.editor
// } .update(cx, |editor, cx| editor.active_match_index(matches, cx))
// } }
}
// TODO FEEDBACK: search buffer?
// TODO FEEDBACK: warnings

View file

@ -36,7 +36,7 @@ pub use workspace;
use workspace::{sidebar::SidebarSide, AppState, Workspace}; use workspace::{sidebar::SidebarSide, AppState, Workspace};
#[derive(Deserialize, Clone, PartialEq)] #[derive(Deserialize, Clone, PartialEq)]
struct OpenBrowser { pub struct OpenBrowser {
url: Arc<str>, url: Arc<str>,
} }