Complete first iteration of in-app feedback
This commit is contained in:
parent
083986dfae
commit
fb2278dc6d
5 changed files with 113 additions and 130 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2035,6 +2035,7 @@ dependencies = [
|
||||||
"isahc",
|
"isahc",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"log",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"search",
|
"search",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue