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 WeakViewHandle,
13};
14use isahc::Request;
15use language::{Language, LanguageConfig};
16use postage::prelude::Stream;
17
18use lazy_static::lazy_static;
19use project::{Project, ProjectEntryId, ProjectPath};
20use serde::Serialize;
21use settings::Settings;
22use smallvec::SmallVec;
23use workspace::{
24 item::{Item, ItemHandle},
25 StatusItemView, Workspace,
26};
27
28use crate::system_specs::SystemSpecs;
29
30lazy_static! {
31 pub static ref ZED_SERVER_URL: String =
32 std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string());
33}
34
35// TODO FEEDBACK: In the future, it would be nice to use this is some sort of live-rendering character counter thing
36// Currently, we are just checking on submit that the the text exceeds the `start` value in this range
37const FEEDBACK_CHAR_COUNT_RANGE: Range<usize> = Range {
38 start: 5,
39 end: 1000,
40};
41
42actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
43
44pub fn init(cx: &mut MutableAppContext) {
45 cx.add_action(FeedbackEditor::deploy);
46}
47
48pub struct FeedbackButton;
49
50impl Entity for FeedbackButton {
51 type Event = ();
52}
53
54impl View for FeedbackButton {
55 fn ui_name() -> &'static str {
56 "FeedbackButton"
57 }
58
59 fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
60 Stack::new()
61 .with_child(
62 MouseEventHandler::<Self>::new(0, cx, |state, cx| {
63 let theme = &cx.global::<Settings>().theme;
64 let theme = &theme.workspace.status_bar.feedback;
65
66 Text::new(
67 "Give Feedback".to_string(),
68 theme.style_for(state, true).clone(),
69 )
70 .boxed()
71 })
72 .with_cursor_style(CursorStyle::PointingHand)
73 .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
74 .boxed(),
75 )
76 .boxed()
77 }
78
79 fn focus_in(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {}
80
81 fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {}
82
83 fn key_down(&mut self, _: &gpui::KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
84 false
85 }
86
87 fn key_up(&mut self, _: &gpui::KeyUpEvent, _: &mut ViewContext<Self>) -> bool {
88 false
89 }
90
91 fn modifiers_changed(
92 &mut self,
93 _: &gpui::ModifiersChangedEvent,
94 _: &mut ViewContext<Self>,
95 ) -> bool {
96 false
97 }
98
99 fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap_matcher::KeymapContext {
100 Self::default_keymap_context()
101 }
102
103 fn default_keymap_context() -> gpui::keymap_matcher::KeymapContext {
104 let mut cx = gpui::keymap_matcher::KeymapContext::default();
105 cx.set.insert(Self::ui_name().into());
106 cx
107 }
108
109 fn debug_json(&self, _: &gpui::AppContext) -> gpui::serde_json::Value {
110 gpui::serde_json::Value::Null
111 }
112
113 fn text_for_range(&self, _: Range<usize>, _: &gpui::AppContext) -> Option<String> {
114 None
115 }
116
117 fn selected_text_range(&self, _: &gpui::AppContext) -> Option<Range<usize>> {
118 None
119 }
120
121 fn marked_text_range(&self, _: &gpui::AppContext) -> Option<Range<usize>> {
122 None
123 }
124
125 fn unmark_text(&mut self, _: &mut ViewContext<Self>) {}
126
127 fn replace_text_in_range(
128 &mut self,
129 _: Option<Range<usize>>,
130 _: &str,
131 _: &mut ViewContext<Self>,
132 ) {
133 }
134
135 fn replace_and_mark_text_in_range(
136 &mut self,
137 _: Option<Range<usize>>,
138 _: &str,
139 _: Option<Range<usize>>,
140 _: &mut ViewContext<Self>,
141 ) {
142 }
143}
144
145impl StatusItemView for FeedbackButton {
146 fn set_active_pane_item(
147 &mut self,
148 _: Option<&dyn ItemHandle>,
149 _: &mut gpui::ViewContext<Self>,
150 ) {
151 }
152}
153
154#[derive(Serialize)]
155struct FeedbackRequestBody<'a> {
156 feedback_text: &'a str,
157 metrics_id: Option<Arc<str>>,
158 system_specs: SystemSpecs,
159 token: &'a str,
160}
161
162#[derive(Clone)]
163struct FeedbackEditor {
164 editor: ViewHandle<Editor>,
165}
166
167impl FeedbackEditor {
168 fn new(
169 project_handle: ModelHandle<Project>,
170 _: WeakViewHandle<Workspace>,
171 cx: &mut ViewContext<Self>,
172 ) -> Self {
173 // TODO FEEDBACK: This doesn't work like I expected it would
174 // let markdown_language = Arc::new(Language::new(
175 // LanguageConfig::default(),
176 // Some(tree_sitter_markdown::language()),
177 // ));
178
179 let markdown_language = project_handle
180 .read(cx)
181 .languages()
182 .get_language("Markdown")
183 .unwrap();
184
185 let buffer = project_handle
186 .update(cx, |project, cx| {
187 project.create_buffer("", Some(markdown_language), cx)
188 })
189 .expect("creating buffers on a local workspace always succeeds");
190
191 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.";
192
193 let editor = cx.add_view(|cx| {
194 let mut editor = Editor::for_buffer(buffer, Some(project_handle.clone()), cx);
195 editor.set_vertical_scroll_margin(5, cx);
196 editor.set_placeholder_text(FEDBACK_PLACEHOLDER_TEXT, cx);
197 editor
198 });
199
200 let this = Self { editor };
201 this
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 workspace_handle = cx.weak_handle();
330 let feedback_editor = cx
331 .add_view(|cx| FeedbackEditor::new(workspace.project().clone(), workspace_handle, cx));
332 workspace.add_item(Box::new(feedback_editor), cx);
333 // }
334 }
335}
336
337impl View for FeedbackEditor {
338 fn ui_name() -> &'static str {
339 "FeedbackEditor"
340 }
341
342 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
343 ChildView::new(&self.editor, cx).boxed()
344 }
345
346 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
347 if cx.is_self_focused() {
348 cx.focus(&self.editor);
349 }
350 }
351}
352
353impl Entity for FeedbackEditor {
354 type Event = ();
355}
356
357impl Item for FeedbackEditor {
358 fn tab_content(
359 &self,
360 _: Option<usize>,
361 style: &theme::Tab,
362 _: &gpui::AppContext,
363 ) -> ElementBox {
364 Flex::row()
365 .with_child(
366 Label::new("Feedback".to_string(), style.label.clone())
367 .aligned()
368 .contained()
369 .boxed(),
370 )
371 .boxed()
372 }
373
374 fn to_item_events(_: &Self::Event) -> Vec<workspace::item::ItemEvent> {
375 Vec::new()
376 }
377
378 fn project_path(&self, _: &gpui::AppContext) -> Option<ProjectPath> {
379 None
380 }
381
382 fn project_entry_ids(&self, _: &gpui::AppContext) -> SmallVec<[ProjectEntryId; 3]> {
383 SmallVec::new()
384 }
385
386 fn is_singleton(&self, _: &gpui::AppContext) -> bool {
387 true
388 }
389
390 fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
391
392 fn can_save(&self, _: &gpui::AppContext) -> bool {
393 true
394 }
395
396 fn save(
397 &mut self,
398 project_handle: gpui::ModelHandle<Project>,
399 cx: &mut ViewContext<Self>,
400 ) -> Task<anyhow::Result<()>> {
401 self.handle_save(project_handle, cx)
402 }
403
404 fn save_as(
405 &mut self,
406 project_handle: gpui::ModelHandle<Project>,
407 _: std::path::PathBuf,
408 cx: &mut ViewContext<Self>,
409 ) -> Task<anyhow::Result<()>> {
410 self.handle_save(project_handle, cx)
411 }
412
413 fn reload(
414 &mut self,
415 _: gpui::ModelHandle<Project>,
416 _: &mut ViewContext<Self>,
417 ) -> Task<anyhow::Result<()>> {
418 unreachable!("reload should not have been called")
419 }
420
421 fn clone_on_split(
422 &self,
423 _workspace_id: workspace::WorkspaceId,
424 cx: &mut ViewContext<Self>,
425 ) -> Option<Self>
426 where
427 Self: Sized,
428 {
429 // TODO FEEDBACK: split is busted
430 // Some(self.clone())
431 None
432 }
433
434 fn serialized_item_kind() -> Option<&'static str> {
435 None
436 }
437
438 fn deserialize(
439 _: gpui::ModelHandle<Project>,
440 _: gpui::WeakViewHandle<Workspace>,
441 _: workspace::WorkspaceId,
442 _: workspace::ItemId,
443 _: &mut ViewContext<workspace::Pane>,
444 ) -> Task<anyhow::Result<ViewHandle<Self>>> {
445 unreachable!()
446 }
447}
448
449// TODO FEEDBACK: search buffer?
450// TODO FEEDBACK: warnings