mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
WIP
This commit is contained in:
parent
f4c7b13397
commit
46c998ca8d
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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();
|
||||
// })
|
||||
// }
|
||||
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)),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user