1use std::{ops::Range, sync::Arc};
2
3use anyhow::bail;
4use client::{Client, ZED_SECRET_CLIENT_TOKEN};
5use editor::Editor;
6use futures::AsyncReadExt;
7use gpui::{
8 actions,
9 elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text},
10 serde_json, AnyViewHandle, CursorStyle, Element, ElementBox, Entity, ModelHandle, MouseButton,
11 MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
12};
13use isahc::Request;
14use language::Buffer;
15use postage::prelude::Stream;
16
17use lazy_static::lazy_static;
18use project::{Project, ProjectEntryId, ProjectPath};
19use serde::Serialize;
20use settings::Settings;
21use smallvec::SmallVec;
22use workspace::{
23 item::{Item, ItemHandle},
24 StatusItemView, Workspace,
25};
26
27use crate::system_specs::SystemSpecs;
28
29lazy_static! {
30 pub static ref ZED_SERVER_URL: String =
31 std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string());
32}
33
34// TODO FEEDBACK: In the future, it would be nice to use this is some sort of live-rendering character counter thing
35// Currently, we are just checking on submit that the the text exceeds the `start` value in this range
36const FEEDBACK_CHAR_COUNT_RANGE: Range<usize> = Range {
37 start: 5,
38 end: 1000,
39};
40
41actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
42
43pub fn init(cx: &mut MutableAppContext) {
44 cx.add_action(FeedbackEditor::deploy);
45}
46
47pub struct FeedbackButton;
48
49impl Entity for FeedbackButton {
50 type Event = ();
51}
52
53impl View for FeedbackButton {
54 fn ui_name() -> &'static str {
55 "FeedbackButton"
56 }
57
58 fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
59 Stack::new()
60 .with_child(
61 MouseEventHandler::<Self>::new(0, cx, |state, cx| {
62 let theme = &cx.global::<Settings>().theme;
63 let theme = &theme.workspace.status_bar.feedback;
64
65 Text::new(
66 "Give Feedback".to_string(),
67 theme.style_for(state, true).clone(),
68 )
69 .boxed()
70 })
71 .with_cursor_style(CursorStyle::PointingHand)
72 .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
73 .boxed(),
74 )
75 .boxed()
76 }
77
78 fn focus_in(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {}
79
80 fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {}
81
82 fn key_down(&mut self, _: &gpui::KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
83 false
84 }
85
86 fn key_up(&mut self, _: &gpui::KeyUpEvent, _: &mut ViewContext<Self>) -> bool {
87 false
88 }
89
90 fn modifiers_changed(
91 &mut self,
92 _: &gpui::ModifiersChangedEvent,
93 _: &mut ViewContext<Self>,
94 ) -> bool {
95 false
96 }
97
98 fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap_matcher::KeymapContext {
99 Self::default_keymap_context()
100 }
101
102 fn default_keymap_context() -> gpui::keymap_matcher::KeymapContext {
103 let mut cx = gpui::keymap_matcher::KeymapContext::default();
104 cx.set.insert(Self::ui_name().into());
105 cx
106 }
107
108 fn debug_json(&self, _: &gpui::AppContext) -> gpui::serde_json::Value {
109 gpui::serde_json::Value::Null
110 }
111
112 fn text_for_range(&self, _: Range<usize>, _: &gpui::AppContext) -> Option<String> {
113 None
114 }
115
116 fn selected_text_range(&self, _: &gpui::AppContext) -> Option<Range<usize>> {
117 None
118 }
119
120 fn marked_text_range(&self, _: &gpui::AppContext) -> Option<Range<usize>> {
121 None
122 }
123
124 fn unmark_text(&mut self, _: &mut ViewContext<Self>) {}
125
126 fn replace_text_in_range(
127 &mut self,
128 _: Option<Range<usize>>,
129 _: &str,
130 _: &mut ViewContext<Self>,
131 ) {
132 }
133
134 fn replace_and_mark_text_in_range(
135 &mut self,
136 _: Option<Range<usize>>,
137 _: &str,
138 _: Option<Range<usize>>,
139 _: &mut ViewContext<Self>,
140 ) {
141 }
142}
143
144impl StatusItemView for FeedbackButton {
145 fn set_active_pane_item(
146 &mut self,
147 _: Option<&dyn ItemHandle>,
148 _: &mut gpui::ViewContext<Self>,
149 ) {
150 }
151}
152
153#[derive(Serialize)]
154struct FeedbackRequestBody<'a> {
155 feedback_text: &'a str,
156 metrics_id: Option<Arc<str>>,
157 system_specs: SystemSpecs,
158 token: &'a str,
159}
160
161#[derive(Clone)]
162struct FeedbackEditor {
163 editor: ViewHandle<Editor>,
164 project: ModelHandle<Project>,
165}
166
167impl FeedbackEditor {
168 fn new_with_buffer(
169 project: ModelHandle<Project>,
170 buffer: ModelHandle<Buffer>,
171 cx: &mut ViewContext<Self>,
172 ) -> Self {
173 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.";
174
175 let editor = cx.add_view(|cx| {
176 let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
177 editor.set_vertical_scroll_margin(5, cx);
178 editor.set_placeholder_text(FEDBACK_PLACEHOLDER_TEXT, cx);
179 editor
180 });
181
182 let this = Self { editor, project };
183 this
184 }
185
186 fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
187 // TODO FEEDBACK: This doesn't work like I expected it would
188 // let markdown_language = Arc::new(Language::new(
189 // LanguageConfig::default(),
190 // Some(tree_sitter_markdown::language()),
191 // ));
192
193 let markdown_language = project.read(cx).languages().get_language("Markdown");
194
195 let buffer = project
196 .update(cx, |project, cx| {
197 project.create_buffer("", markdown_language, cx)
198 })
199 .expect("creating buffers on a local workspace always succeeds");
200
201 Self::new_with_buffer(project, buffer, cx)
202 }
203
204 fn handle_save(
205 &mut self,
206 _: gpui::ModelHandle<Project>,
207 cx: &mut ViewContext<Self>,
208 ) -> Task<anyhow::Result<()>> {
209 // TODO FEEDBACK: These don't look right
210 let feedback_text_length = self.editor.read(cx).buffer().read(cx).len(cx);
211
212 if feedback_text_length <= FEEDBACK_CHAR_COUNT_RANGE.start {
213 cx.prompt(
214 PromptLevel::Critical,
215 &format!(
216 "Feedback must be longer than {} characters",
217 FEEDBACK_CHAR_COUNT_RANGE.start
218 ),
219 &["OK"],
220 );
221
222 return Task::ready(Ok(()));
223 }
224
225 let mut answer = cx.prompt(
226 PromptLevel::Warning,
227 "Ready to submit your feedback?",
228 &["Yes, Submit!", "No"],
229 );
230
231 let this = cx.handle();
232 cx.spawn(|_, mut cx| async move {
233 let answer = answer.recv().await;
234
235 if answer == Some(0) {
236 cx.update(|cx| {
237 this.update(cx, |this, cx| match this.submit_feedback(cx) {
238 // TODO FEEDBACK
239 Ok(_) => {
240 // Close file after feedback sent successfully
241 // workspace
242 // .update(cx, |workspace, cx| {
243 // Pane::close_active_item(workspace, &Default::default(), cx)
244 // .unwrap()
245 // })
246 // .await
247 // .unwrap();
248 }
249 Err(error) => {
250 cx.prompt(PromptLevel::Critical, &error.to_string(), &["OK"]);
251 // Prompt that something failed (and to check the log for the exact error? and to try again?)
252 }
253 })
254 })
255 }
256 })
257 .detach();
258
259 Task::ready(Ok(()))
260 }
261
262 fn submit_feedback(&mut self, cx: &mut ViewContext<'_, Self>) -> anyhow::Result<()> {
263 let feedback_text = self.editor.read(cx).text(cx);
264 let zed_client = cx.global::<Arc<Client>>();
265 let system_specs = SystemSpecs::new(cx);
266 let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
267
268 let metrics_id = zed_client.metrics_id();
269 let http_client = zed_client.http_client();
270
271 // TODO FEEDBACK: how to get error out of the thread
272
273 let this = cx.handle();
274
275 cx.spawn(|_, async_cx| {
276 async move {
277 let request = FeedbackRequestBody {
278 feedback_text: &feedback_text,
279 metrics_id,
280 system_specs,
281 token: ZED_SECRET_CLIENT_TOKEN,
282 };
283
284 let json_bytes = serde_json::to_vec(&request)?;
285
286 let request = Request::post(feedback_endpoint)
287 .header("content-type", "application/json")
288 .body(json_bytes.into())?;
289
290 let mut response = http_client.send(request).await?;
291 let mut body = String::new();
292 response.body_mut().read_to_string(&mut body).await?;
293
294 let response_status = response.status();
295
296 if !response_status.is_success() {
297 bail!("Feedback API failed with: {}", response_status)
298 }
299
300 this.read_with(&async_cx, |_this, _cx| -> anyhow::Result<()> {
301 bail!("Error")
302 })?;
303
304 // TODO FEEDBACK: Use or remove
305 // Will need to handle error cases
306 // async_cx.update(|cx| {
307 // this.update(cx, |this, cx| {
308 // this.handle_error(error);
309 // cx.notify();
310 // cx.dispatch_action(ShowErrorPopover);
311 // this.error_text = "Embedding failed"
312 // })
313 // });
314
315 Ok(())
316 }
317 })
318 .detach();
319
320 Ok(())
321 }
322}
323
324impl FeedbackEditor {
325 pub fn deploy(workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>) {
326 // if let Some(existing) = workspace.item_of_type::<FeedbackEditor>(cx) {
327 // workspace.activate_item(&existing, cx);
328 // } else {
329 let feedback_editor =
330 cx.add_view(|cx| FeedbackEditor::new(workspace.project().clone(), cx));
331 workspace.add_item(Box::new(feedback_editor), cx);
332 // }
333 }
334}
335
336impl View for FeedbackEditor {
337 fn ui_name() -> &'static str {
338 "FeedbackEditor"
339 }
340
341 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
342 ChildView::new(&self.editor, cx).boxed()
343 }
344
345 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
346 if cx.is_self_focused() {
347 cx.focus(&self.editor);
348 }
349 }
350}
351
352impl Entity for FeedbackEditor {
353 type Event = ();
354}
355
356impl Item for FeedbackEditor {
357 fn tab_content(
358 &self,
359 _: Option<usize>,
360 style: &theme::Tab,
361 _: &gpui::AppContext,
362 ) -> ElementBox {
363 Flex::row()
364 .with_child(
365 Label::new("Feedback".to_string(), style.label.clone())
366 .aligned()
367 .contained()
368 .boxed(),
369 )
370 .boxed()
371 }
372
373 fn to_item_events(_: &Self::Event) -> Vec<workspace::item::ItemEvent> {
374 Vec::new()
375 }
376
377 fn project_path(&self, _: &gpui::AppContext) -> Option<ProjectPath> {
378 None
379 }
380
381 fn project_entry_ids(&self, _: &gpui::AppContext) -> SmallVec<[ProjectEntryId; 3]> {
382 SmallVec::new()
383 }
384
385 fn is_singleton(&self, _: &gpui::AppContext) -> bool {
386 true
387 }
388
389 fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
390
391 fn can_save(&self, _: &gpui::AppContext) -> bool {
392 true
393 }
394
395 fn save(
396 &mut self,
397 project: gpui::ModelHandle<Project>,
398 cx: &mut ViewContext<Self>,
399 ) -> Task<anyhow::Result<()>> {
400 self.handle_save(project, cx)
401 }
402
403 fn save_as(
404 &mut self,
405 project: gpui::ModelHandle<Project>,
406 _: std::path::PathBuf,
407 cx: &mut ViewContext<Self>,
408 ) -> Task<anyhow::Result<()>> {
409 self.handle_save(project, cx)
410 }
411
412 fn reload(
413 &mut self,
414 _: gpui::ModelHandle<Project>,
415 _: &mut ViewContext<Self>,
416 ) -> Task<anyhow::Result<()>> {
417 unreachable!("reload should not have been called")
418 }
419
420 fn clone_on_split(
421 &self,
422 _workspace_id: workspace::WorkspaceId,
423 cx: &mut ViewContext<Self>,
424 ) -> Option<Self>
425 where
426 Self: Sized,
427 {
428 let buffer = self
429 .editor
430 .read(cx)
431 .buffer()
432 .read(cx)
433 .as_singleton()
434 .expect("Feedback buffer is only ever singleton");
435
436 Some(Self::new_with_buffer(
437 self.project.clone(),
438 buffer.clone(),
439 cx,
440 ))
441 }
442
443 fn serialized_item_kind() -> Option<&'static str> {
444 None
445 }
446
447 fn deserialize(
448 _: gpui::ModelHandle<Project>,
449 _: gpui::WeakViewHandle<Workspace>,
450 _: workspace::WorkspaceId,
451 _: workspace::ItemId,
452 _: &mut ViewContext<workspace::Pane>,
453 ) -> Task<anyhow::Result<ViewHandle<Self>>> {
454 unreachable!()
455 }
456}
457
458// impl SearchableItem for FeedbackEditor {
459// type Match = <Editor as SearchableItem>::Match;
460
461// fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
462// Editor::to_search_event(event)
463// }
464
465// fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
466// self.
467// }
468
469// fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
470// todo!()
471// }
472
473// fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
474// todo!()
475// }
476
477// fn activate_match(
478// &mut self,
479// index: usize,
480// matches: Vec<Self::Match>,
481// cx: &mut ViewContext<Self>,
482// ) {
483// todo!()
484// }
485
486// fn find_matches(
487// &mut self,
488// query: project::search::SearchQuery,
489// cx: &mut ViewContext<Self>,
490// ) -> Task<Vec<Self::Match>> {
491// todo!()
492// }
493
494// fn active_match_index(
495// &mut self,
496// matches: Vec<Self::Match>,
497// cx: &mut ViewContext<Self>,
498// ) -> Option<usize> {
499// todo!()
500// }
501// }
502
503// TODO FEEDBACK: search buffer?
504// TODO FEEDBACK: warnings