1// use crate::system_specs::SystemSpecs;
2// use anyhow::bail;
3// use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
4// use editor::{Anchor, Editor};
5// use futures::AsyncReadExt;
6// use gpui::{
7// actions,
8// elements::{ChildView, Flex, Label, ParentElement, Svg},
9// platform::PromptLevel,
10// serde_json, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View,
11// ViewContext, ViewHandle,
12// };
13// use isahc::Request;
14// use language::Buffer;
15// use postage::prelude::Stream;
16// use project::{search::SearchQuery, Project};
17// use regex::Regex;
18// use serde::Serialize;
19// use smallvec::SmallVec;
20// use std::{
21// any::TypeId,
22// borrow::Cow,
23// ops::{Range, RangeInclusive},
24// sync::Arc,
25// };
26// use util::ResultExt;
27// use workspace::{
28// item::{Item, ItemEvent, ItemHandle},
29// searchable::{SearchableItem, SearchableItemHandle},
30// Workspace,
31// };
32
33// const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
34// const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
35// "Feedback failed to submit, see error log for details.";
36
37// actions!(feedback, [GiveFeedback, SubmitFeedback]);
38
39// pub fn init(cx: &mut AppContext) {
40// cx.add_action({
41// move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
42// FeedbackEditor::deploy(workspace, cx);
43// }
44// });
45// }
46
47// #[derive(Serialize)]
48// struct FeedbackRequestBody<'a> {
49// feedback_text: &'a str,
50// email: Option<String>,
51// metrics_id: Option<Arc<str>>,
52// installation_id: Option<Arc<str>>,
53// system_specs: SystemSpecs,
54// is_staff: bool,
55// token: &'a str,
56// }
57
58// #[derive(Clone)]
59// pub(crate) struct FeedbackEditor {
60// system_specs: SystemSpecs,
61// editor: ViewHandle<Editor>,
62// project: ModelHandle<Project>,
63// pub allow_submission: bool,
64// }
65
66// impl FeedbackEditor {
67// fn new(
68// system_specs: SystemSpecs,
69// project: ModelHandle<Project>,
70// buffer: ModelHandle<Buffer>,
71// cx: &mut ViewContext<Self>,
72// ) -> Self {
73// let editor = cx.add_view(|cx| {
74// let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
75// editor.set_vertical_scroll_margin(5, cx);
76// editor
77// });
78
79// cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()))
80// .detach();
81
82// Self {
83// system_specs: system_specs.clone(),
84// editor,
85// project,
86// allow_submission: true,
87// }
88// }
89
90// pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
91// if !self.allow_submission {
92// return Task::ready(Ok(()));
93// }
94
95// let feedback_text = self.editor.read(cx).text(cx);
96// let feedback_char_count = feedback_text.chars().count();
97// let feedback_text = feedback_text.trim().to_string();
98
99// let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
100// Some(format!(
101// "Feedback can't be shorter than {} characters.",
102// FEEDBACK_CHAR_LIMIT.start()
103// ))
104// } else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() {
105// Some(format!(
106// "Feedback can't be longer than {} characters.",
107// FEEDBACK_CHAR_LIMIT.end()
108// ))
109// } else {
110// None
111// };
112
113// if let Some(error) = error {
114// cx.prompt(PromptLevel::Critical, &error, &["OK"]);
115// return Task::ready(Ok(()));
116// }
117
118// let mut answer = cx.prompt(
119// PromptLevel::Info,
120// "Ready to submit your feedback?",
121// &["Yes, Submit!", "No"],
122// );
123
124// let client = cx.global::<Arc<Client>>().clone();
125// let specs = self.system_specs.clone();
126
127// cx.spawn(|this, mut cx| async move {
128// let answer = answer.recv().await;
129
130// if answer == Some(0) {
131// this.update(&mut cx, |feedback_editor, cx| {
132// feedback_editor.set_allow_submission(false, cx);
133// })
134// .log_err();
135
136// match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
137// Ok(_) => {
138// this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
139// .log_err();
140// }
141
142// Err(error) => {
143// log::error!("{}", error);
144// this.update(&mut cx, |feedback_editor, cx| {
145// cx.prompt(
146// PromptLevel::Critical,
147// FEEDBACK_SUBMISSION_ERROR_TEXT,
148// &["OK"],
149// );
150// feedback_editor.set_allow_submission(true, cx);
151// })
152// .log_err();
153// }
154// }
155// }
156// })
157// .detach();
158
159// Task::ready(Ok(()))
160// }
161
162// fn set_allow_submission(&mut self, allow_submission: bool, cx: &mut ViewContext<Self>) {
163// self.allow_submission = allow_submission;
164// cx.notify();
165// }
166
167// async fn submit_feedback(
168// feedback_text: &str,
169// zed_client: Arc<Client>,
170// system_specs: SystemSpecs,
171// ) -> anyhow::Result<()> {
172// let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
173
174// let telemetry = zed_client.telemetry();
175// let metrics_id = telemetry.metrics_id();
176// let installation_id = telemetry.installation_id();
177// let is_staff = telemetry.is_staff();
178// let http_client = zed_client.http_client();
179
180// let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
181
182// let emails: Vec<&str> = re
183// .captures_iter(feedback_text)
184// .map(|capture| capture.get(0).unwrap().as_str())
185// .collect();
186
187// let email = emails.first().map(|e| e.to_string());
188
189// let request = FeedbackRequestBody {
190// feedback_text: &feedback_text,
191// email,
192// metrics_id,
193// installation_id,
194// system_specs,
195// is_staff: is_staff.unwrap_or(false),
196// token: ZED_SECRET_CLIENT_TOKEN,
197// };
198
199// let json_bytes = serde_json::to_vec(&request)?;
200
201// let request = Request::post(feedback_endpoint)
202// .header("content-type", "application/json")
203// .body(json_bytes.into())?;
204
205// let mut response = http_client.send(request).await?;
206// let mut body = String::new();
207// response.body_mut().read_to_string(&mut body).await?;
208
209// let response_status = response.status();
210
211// if !response_status.is_success() {
212// bail!("Feedback API failed with error: {}", response_status)
213// }
214
215// Ok(())
216// }
217// }
218
219// impl FeedbackEditor {
220// pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
221// let markdown = workspace
222// .app_state()
223// .languages
224// .language_for_name("Markdown");
225// cx.spawn(|workspace, mut cx| async move {
226// let markdown = markdown.await.log_err();
227// workspace
228// .update(&mut cx, |workspace, cx| {
229// workspace.with_local_workspace(cx, |workspace, cx| {
230// let project = workspace.project().clone();
231// let buffer = project
232// .update(cx, |project, cx| project.create_buffer("", markdown, cx))
233// .expect("creating buffers on a local workspace always succeeds");
234// let system_specs = SystemSpecs::new(cx);
235// let feedback_editor = cx
236// .add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
237// workspace.add_item(Box::new(feedback_editor), cx);
238// })
239// })?
240// .await
241// })
242// .detach_and_log_err(cx);
243// }
244// }
245
246// impl View for FeedbackEditor {
247// fn ui_name() -> &'static str {
248// "FeedbackEditor"
249// }
250
251// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
252// ChildView::new(&self.editor, cx).into_any()
253// }
254
255// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
256// if cx.is_self_focused() {
257// cx.focus(&self.editor);
258// }
259// }
260// }
261
262// impl Entity for FeedbackEditor {
263// type Event = editor::Event;
264// }
265
266// impl Item for FeedbackEditor {
267// fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
268// Some("Send Feedback".into())
269// }
270
271// fn tab_content<T: 'static>(
272// &self,
273// _: Option<usize>,
274// style: &theme::Tab,
275// _: &AppContext,
276// ) -> AnyElement<T> {
277// Flex::row()
278// .with_child(
279// Svg::new("icons/feedback.svg")
280// .with_color(style.label.text.color)
281// .constrained()
282// .with_width(style.type_icon_width)
283// .aligned()
284// .contained()
285// .with_margin_right(style.spacing),
286// )
287// .with_child(
288// Label::new("Send Feedback", style.label.clone())
289// .aligned()
290// .contained(),
291// )
292// .into_any()
293// }
294
295// fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
296// self.editor.for_each_project_item(cx, f)
297// }
298
299// fn is_singleton(&self, _: &AppContext) -> bool {
300// true
301// }
302
303// fn can_save(&self, _: &AppContext) -> bool {
304// true
305// }
306
307// fn save(
308// &mut self,
309// _: ModelHandle<Project>,
310// cx: &mut ViewContext<Self>,
311// ) -> Task<anyhow::Result<()>> {
312// self.submit(cx)
313// }
314
315// fn save_as(
316// &mut self,
317// _: ModelHandle<Project>,
318// _: std::path::PathBuf,
319// cx: &mut ViewContext<Self>,
320// ) -> Task<anyhow::Result<()>> {
321// self.submit(cx)
322// }
323
324// fn reload(
325// &mut self,
326// _: ModelHandle<Project>,
327// _: &mut ViewContext<Self>,
328// ) -> Task<anyhow::Result<()>> {
329// Task::Ready(Some(Ok(())))
330// }
331
332// fn clone_on_split(
333// &self,
334// _workspace_id: workspace::WorkspaceId,
335// cx: &mut ViewContext<Self>,
336// ) -> Option<Self>
337// where
338// Self: Sized,
339// {
340// let buffer = self
341// .editor
342// .read(cx)
343// .buffer()
344// .read(cx)
345// .as_singleton()
346// .expect("Feedback buffer is only ever singleton");
347
348// Some(Self::new(
349// self.system_specs.clone(),
350// self.project.clone(),
351// buffer.clone(),
352// cx,
353// ))
354// }
355
356// fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
357// Some(Box::new(handle.clone()))
358// }
359
360// fn act_as_type<'a>(
361// &'a self,
362// type_id: TypeId,
363// self_handle: &'a ViewHandle<Self>,
364// _: &'a AppContext,
365// ) -> Option<&'a AnyViewHandle> {
366// if type_id == TypeId::of::<Self>() {
367// Some(self_handle)
368// } else if type_id == TypeId::of::<Editor>() {
369// Some(&self.editor)
370// } else {
371// None
372// }
373// }
374
375// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
376// Editor::to_item_events(event)
377// }
378// }
379
380// impl SearchableItem for FeedbackEditor {
381// type Match = Range<Anchor>;
382
383// fn to_search_event(
384// &mut self,
385// event: &Self::Event,
386// cx: &mut ViewContext<Self>,
387// ) -> Option<workspace::searchable::SearchEvent> {
388// self.editor
389// .update(cx, |editor, cx| editor.to_search_event(event, cx))
390// }
391
392// fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
393// self.editor
394// .update(cx, |editor, cx| editor.clear_matches(cx))
395// }
396
397// fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
398// self.editor
399// .update(cx, |editor, cx| editor.update_matches(matches, cx))
400// }
401
402// fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
403// self.editor
404// .update(cx, |editor, cx| editor.query_suggestion(cx))
405// }
406
407// fn activate_match(
408// &mut self,
409// index: usize,
410// matches: Vec<Self::Match>,
411// cx: &mut ViewContext<Self>,
412// ) {
413// self.editor
414// .update(cx, |editor, cx| editor.activate_match(index, matches, cx))
415// }
416
417// fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
418// self.editor
419// .update(cx, |e, cx| e.select_matches(matches, cx))
420// }
421// fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext<Self>) {
422// self.editor
423// .update(cx, |e, cx| e.replace(matches, query, cx));
424// }
425// fn find_matches(
426// &mut self,
427// query: Arc<project::search::SearchQuery>,
428// cx: &mut ViewContext<Self>,
429// ) -> Task<Vec<Self::Match>> {
430// self.editor
431// .update(cx, |editor, cx| editor.find_matches(query, cx))
432// }
433
434// fn active_match_index(
435// &mut self,
436// matches: Vec<Self::Match>,
437// cx: &mut ViewContext<Self>,
438// ) -> Option<usize> {
439// self.editor
440// .update(cx, |editor, cx| editor.active_match_index(matches, cx))
441// }
442// }