1use buffer_diff::BufferDiff;
2use edit_prediction::{EditPrediction, EditPredictionRating, EditPredictionStore};
3use editor::{Editor, Inlay, MultiBuffer};
4use feature_flags::FeatureFlag;
5use gpui::{
6 App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
7 Length, StyleRefinement, TextStyleRefinement, Window, actions, prelude::*,
8};
9use language::{Buffer, CodeLabel, LanguageRegistry, Point, ToOffset, language_settings};
10use markdown::{Markdown, MarkdownStyle};
11use project::{
12 Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, InlayId,
13};
14use settings::Settings as _;
15use std::rc::Rc;
16use std::{fmt::Write, sync::Arc};
17use theme_settings::ThemeSettings;
18use ui::{
19 ContextMenu, DropdownMenu, KeyBinding, List, ListItem, ListItemSpacing, PopoverMenuHandle,
20 Tooltip, prelude::*,
21};
22use workspace::{ModalView, Workspace};
23
24actions!(
25 zeta,
26 [
27 /// Rates the active completion with a thumbs up.
28 ThumbsUpActivePrediction,
29 /// Rates the active completion with a thumbs down.
30 ThumbsDownActivePrediction,
31 /// Navigates to the next edit in the completion history.
32 NextEdit,
33 /// Navigates to the previous edit in the completion history.
34 PreviousEdit,
35 /// Focuses on the completions list.
36 FocusPredictions,
37 /// Previews the selected completion.
38 PreviewPrediction,
39 ]
40);
41
42pub struct PredictEditsRatePredictionsFeatureFlag;
43
44impl FeatureFlag for PredictEditsRatePredictionsFeatureFlag {
45 const NAME: &'static str = "predict-edits-rate-completions";
46}
47
48pub struct RatePredictionsModal {
49 ep_store: Entity<EditPredictionStore>,
50 language_registry: Arc<LanguageRegistry>,
51 active_prediction: Option<ActivePrediction>,
52 selected_index: usize,
53 diff_editor: Entity<Editor>,
54 focus_handle: FocusHandle,
55 _subscription: gpui::Subscription,
56 current_view: RatePredictionView,
57 failure_mode_menu_handle: PopoverMenuHandle<ContextMenu>,
58}
59
60struct ActivePrediction {
61 prediction: EditPrediction,
62 feedback_editor: Entity<Editor>,
63 formatted_inputs: Entity<Markdown>,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
67enum RatePredictionView {
68 SuggestedEdits,
69 RawInput,
70}
71
72impl RatePredictionView {
73 pub fn name(&self) -> &'static str {
74 match self {
75 Self::SuggestedEdits => "Suggested Edits",
76 Self::RawInput => "Recorded Events & Input",
77 }
78 }
79}
80
81impl RatePredictionsModal {
82 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
83 if let Some(ep_store) = EditPredictionStore::try_global(cx) {
84 let language_registry = workspace.app_state().languages.clone();
85 workspace.toggle_modal(window, cx, |window, cx| {
86 RatePredictionsModal::new(ep_store, language_registry, window, cx)
87 });
88
89 telemetry::event!("Rate Prediction Modal Open", source = "Edit Prediction");
90 }
91 }
92
93 pub fn new(
94 ep_store: Entity<EditPredictionStore>,
95 language_registry: Arc<LanguageRegistry>,
96 window: &mut Window,
97 cx: &mut Context<Self>,
98 ) -> Self {
99 let subscription = cx.observe(&ep_store, |_, _, cx| cx.notify());
100
101 Self {
102 ep_store,
103 language_registry,
104 selected_index: 0,
105 focus_handle: cx.focus_handle(),
106 active_prediction: None,
107 _subscription: subscription,
108 diff_editor: cx.new(|cx| {
109 let multibuffer = cx.new(|_| MultiBuffer::new(language::Capability::ReadOnly));
110 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
111 editor.disable_inline_diagnostics();
112 editor.set_expand_all_diff_hunks(cx);
113 editor.set_show_git_diff_gutter(false, cx);
114 editor
115 }),
116 current_view: RatePredictionView::SuggestedEdits,
117 failure_mode_menu_handle: PopoverMenuHandle::default(),
118 }
119 }
120
121 fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
122 cx.emit(DismissEvent);
123 }
124
125 fn select_next(&mut self, _: &menu::SelectNext, _: &mut Window, cx: &mut Context<Self>) {
126 self.selected_index += 1;
127 self.selected_index = usize::min(
128 self.selected_index,
129 self.ep_store.read(cx).shown_predictions().count(),
130 );
131 cx.notify();
132 }
133
134 fn select_previous(
135 &mut self,
136 _: &menu::SelectPrevious,
137 _: &mut Window,
138 cx: &mut Context<Self>,
139 ) {
140 self.selected_index = self.selected_index.saturating_sub(1);
141 cx.notify();
142 }
143
144 fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {
145 let next_index = self
146 .ep_store
147 .read(cx)
148 .shown_predictions()
149 .skip(self.selected_index)
150 .enumerate()
151 .skip(1) // Skip straight to the next item
152 .find(|(_, completion)| !completion.edits.is_empty())
153 .map(|(ix, _)| ix + self.selected_index);
154
155 if let Some(next_index) = next_index {
156 self.selected_index = next_index;
157 cx.notify();
158 }
159 }
160
161 fn select_prev_edit(&mut self, _: &PreviousEdit, _: &mut Window, cx: &mut Context<Self>) {
162 let ep_store = self.ep_store.read(cx);
163 let completions_len = ep_store.shown_completions_len();
164
165 let prev_index = self
166 .ep_store
167 .read(cx)
168 .shown_predictions()
169 .rev()
170 .skip((completions_len - 1) - self.selected_index)
171 .enumerate()
172 .skip(1) // Skip straight to the previous item
173 .find(|(_, completion)| !completion.edits.is_empty())
174 .map(|(ix, _)| self.selected_index - ix);
175
176 if let Some(prev_index) = prev_index {
177 self.selected_index = prev_index;
178 cx.notify();
179 }
180 cx.notify();
181 }
182
183 fn select_first(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
184 self.selected_index = 0;
185 cx.notify();
186 }
187
188 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
189 self.selected_index = self.ep_store.read(cx).shown_completions_len() - 1;
190 cx.notify();
191 }
192
193 pub fn thumbs_up_active(
194 &mut self,
195 _: &ThumbsUpActivePrediction,
196 window: &mut Window,
197 cx: &mut Context<Self>,
198 ) {
199 self.ep_store.update(cx, |ep_store, cx| {
200 if let Some(active) = &self.active_prediction {
201 ep_store.rate_prediction(
202 &active.prediction,
203 EditPredictionRating::Positive,
204 active.feedback_editor.read(cx).text(cx),
205 cx,
206 );
207 }
208 });
209
210 let current_completion = self
211 .active_prediction
212 .as_ref()
213 .map(|completion| completion.prediction.clone());
214 self.select_completion(current_completion, false, window, cx);
215 self.select_next_edit(&Default::default(), window, cx);
216 self.confirm(&Default::default(), window, cx);
217
218 cx.notify();
219 }
220
221 pub fn thumbs_down_active(
222 &mut self,
223 _: &ThumbsDownActivePrediction,
224 window: &mut Window,
225 cx: &mut Context<Self>,
226 ) {
227 if let Some(active) = &self.active_prediction {
228 if active.feedback_editor.read(cx).text(cx).is_empty() {
229 return;
230 }
231
232 self.ep_store.update(cx, |ep_store, cx| {
233 ep_store.rate_prediction(
234 &active.prediction,
235 EditPredictionRating::Negative,
236 active.feedback_editor.read(cx).text(cx),
237 cx,
238 );
239 });
240 }
241
242 let current_completion = self
243 .active_prediction
244 .as_ref()
245 .map(|completion| completion.prediction.clone());
246 self.select_completion(current_completion, false, window, cx);
247 self.select_next_edit(&Default::default(), window, cx);
248 self.confirm(&Default::default(), window, cx);
249
250 cx.notify();
251 }
252
253 fn focus_completions(
254 &mut self,
255 _: &FocusPredictions,
256 window: &mut Window,
257 cx: &mut Context<Self>,
258 ) {
259 cx.focus_self(window);
260 cx.notify();
261 }
262
263 fn preview_completion(
264 &mut self,
265 _: &PreviewPrediction,
266 window: &mut Window,
267 cx: &mut Context<Self>,
268 ) {
269 let completion = self
270 .ep_store
271 .read(cx)
272 .shown_predictions()
273 .skip(self.selected_index)
274 .take(1)
275 .next()
276 .cloned();
277
278 self.select_completion(completion, false, window, cx);
279 }
280
281 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
282 let completion = self
283 .ep_store
284 .read(cx)
285 .shown_predictions()
286 .skip(self.selected_index)
287 .take(1)
288 .next()
289 .cloned();
290
291 self.select_completion(completion, true, window, cx);
292 }
293
294 pub fn select_completion(
295 &mut self,
296 prediction: Option<EditPrediction>,
297 focus: bool,
298 window: &mut Window,
299 cx: &mut Context<Self>,
300 ) {
301 // Avoid resetting completion rating if it's already selected.
302 if let Some(prediction) = prediction {
303 self.selected_index = self
304 .ep_store
305 .read(cx)
306 .shown_predictions()
307 .enumerate()
308 .find(|(_, completion_b)| prediction.id == completion_b.id)
309 .map(|(ix, _)| ix)
310 .unwrap_or(self.selected_index);
311 cx.notify();
312
313 if let Some(prev_prediction) = self.active_prediction.as_ref()
314 && prediction.id == prev_prediction.prediction.id
315 {
316 if focus {
317 window.focus(&prev_prediction.feedback_editor.focus_handle(cx), cx);
318 }
319 return;
320 }
321
322 self.diff_editor.update(cx, |editor, cx| {
323 let new_buffer = prediction.edit_preview.build_result_buffer(cx);
324 let new_buffer_snapshot = new_buffer.read(cx).snapshot();
325 let old_buffer_snapshot = prediction.snapshot.clone();
326 let new_buffer_id = new_buffer_snapshot.remote_id();
327
328 let range = prediction
329 .edit_preview
330 .compute_visible_range(&prediction.edits)
331 .unwrap_or(Point::zero()..Point::zero());
332 let start = Point::new(range.start.row.saturating_sub(5), 0);
333 let end = Point::new(range.end.row + 5, 0).min(new_buffer_snapshot.max_point());
334
335 let language = new_buffer_snapshot.language().cloned();
336 let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx));
337 diff.update(cx, |diff, cx| {
338 let update = diff.update_diff(
339 new_buffer_snapshot.text.clone(),
340 Some(old_buffer_snapshot.text().into()),
341 Some(true),
342 language,
343 cx,
344 );
345 cx.spawn(async move |diff, cx| {
346 let update = update.await;
347 if let Some(task) = diff
348 .update(cx, |diff, cx| {
349 diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
350 })
351 .ok()
352 {
353 task.await;
354 }
355 })
356 .detach();
357 });
358
359 editor.disable_header_for_buffer(new_buffer_id, cx);
360 editor.buffer().update(cx, |multibuffer, cx| {
361 multibuffer.clear(cx);
362 multibuffer.set_excerpts_for_buffer(new_buffer.clone(), [start..end], 0, cx);
363 multibuffer.add_diff(diff, cx);
364 });
365
366 if let Some(cursor_position) = prediction.cursor_position.as_ref() {
367 let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx);
368 let cursor_offset = prediction
369 .edit_preview
370 .anchor_to_offset_in_result(cursor_position.anchor)
371 + cursor_position.offset;
372 let cursor_anchor = new_buffer.read(cx).snapshot().anchor_after(cursor_offset);
373
374 if let Some(anchor) = multibuffer_snapshot.anchor_in_excerpt(cursor_anchor) {
375 editor.splice_inlays(
376 &[InlayId::EditPrediction(0)],
377 vec![Inlay::edit_prediction(0, anchor, "▏")],
378 cx,
379 );
380 }
381 }
382 });
383
384 let mut formatted_inputs = String::new();
385
386 write!(&mut formatted_inputs, "## Events\n\n").unwrap();
387
388 for event in &prediction.inputs.events {
389 formatted_inputs.push_str("```diff\n");
390 zeta_prompt::write_event(&mut formatted_inputs, event.as_ref());
391 formatted_inputs.push_str("```\n\n");
392 }
393
394 write!(&mut formatted_inputs, "## Related files\n\n").unwrap();
395
396 for included_file in prediction
397 .inputs
398 .related_files
399 .as_deref()
400 .unwrap_or_default()
401 .iter()
402 {
403 write!(
404 &mut formatted_inputs,
405 "### {}\n\n",
406 included_file.path.display()
407 )
408 .unwrap();
409
410 for excerpt in included_file.excerpts.iter() {
411 write!(
412 &mut formatted_inputs,
413 "```{}\n{}\n```\n",
414 included_file.path.display(),
415 excerpt.text
416 )
417 .unwrap();
418 }
419 }
420
421 write!(&mut formatted_inputs, "## Cursor Excerpt\n\n").unwrap();
422
423 writeln!(
424 &mut formatted_inputs,
425 "```{}\n{}<CURSOR>{}\n```\n",
426 prediction.inputs.cursor_path.display(),
427 &prediction.inputs.cursor_excerpt[..prediction.inputs.cursor_offset_in_excerpt],
428 &prediction.inputs.cursor_excerpt[prediction.inputs.cursor_offset_in_excerpt..],
429 )
430 .unwrap();
431
432 self.active_prediction = Some(ActivePrediction {
433 prediction,
434 feedback_editor: cx.new(|cx| {
435 let mut editor = Editor::multi_line(window, cx);
436 editor.disable_scrollbars_and_minimap(window, cx);
437 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
438 editor.set_show_line_numbers(false, cx);
439 editor.set_show_git_diff_gutter(false, cx);
440 editor.set_show_code_actions(false, cx);
441 editor.set_show_runnables(false, cx);
442 editor.set_show_breakpoints(false, cx);
443 editor.set_show_wrap_guides(false, cx);
444 editor.set_show_indent_guides(false, cx);
445 editor.set_show_edit_predictions(Some(false), window, cx);
446 editor.set_placeholder_text("Add your feedback…", window, cx);
447 editor.set_completion_provider(Some(Rc::new(FeedbackCompletionProvider)));
448 if focus {
449 cx.focus_self(window);
450 }
451 editor
452 }),
453 formatted_inputs: cx.new(|cx| {
454 Markdown::new(
455 formatted_inputs.into(),
456 Some(self.language_registry.clone()),
457 None,
458 cx,
459 )
460 }),
461 });
462 } else {
463 self.active_prediction = None;
464 }
465
466 cx.notify();
467 }
468
469 fn render_view_nav(&self, cx: &Context<Self>) -> impl IntoElement {
470 h_flex()
471 .h_8()
472 .px_1()
473 .border_b_1()
474 .border_color(cx.theme().colors().border)
475 .bg(cx.theme().colors().elevated_surface_background)
476 .gap_1()
477 .child(
478 Button::new(
479 ElementId::Name("suggested-edits".into()),
480 RatePredictionView::SuggestedEdits.name(),
481 )
482 .label_size(LabelSize::Small)
483 .on_click(cx.listener(move |this, _, _window, cx| {
484 this.current_view = RatePredictionView::SuggestedEdits;
485 cx.notify();
486 }))
487 .toggle_state(self.current_view == RatePredictionView::SuggestedEdits),
488 )
489 .child(
490 Button::new(
491 ElementId::Name("raw-input".into()),
492 RatePredictionView::RawInput.name(),
493 )
494 .label_size(LabelSize::Small)
495 .on_click(cx.listener(move |this, _, _window, cx| {
496 this.current_view = RatePredictionView::RawInput;
497 cx.notify();
498 }))
499 .toggle_state(self.current_view == RatePredictionView::RawInput),
500 )
501 }
502
503 fn render_suggested_edits(&self, cx: &mut Context<Self>) -> Option<gpui::Stateful<Div>> {
504 let bg_color = cx.theme().colors().editor_background;
505 Some(
506 div()
507 .id("diff")
508 .p_4()
509 .size_full()
510 .bg(bg_color)
511 .overflow_scroll()
512 .whitespace_nowrap()
513 .child(self.diff_editor.clone()),
514 )
515 }
516
517 fn render_raw_input(
518 &self,
519 window: &mut Window,
520 cx: &mut Context<Self>,
521 ) -> Option<gpui::Stateful<Div>> {
522 let theme_settings = ThemeSettings::get_global(cx);
523 let buffer_font_size = theme_settings.buffer_font_size(cx);
524
525 Some(
526 v_flex()
527 .size_full()
528 .overflow_hidden()
529 .relative()
530 .child(
531 div()
532 .id("raw-input")
533 .py_4()
534 .px_6()
535 .size_full()
536 .bg(cx.theme().colors().editor_background)
537 .overflow_scroll()
538 .child(if let Some(active_prediction) = &self.active_prediction {
539 markdown::MarkdownElement::new(
540 active_prediction.formatted_inputs.clone(),
541 MarkdownStyle {
542 base_text_style: window.text_style(),
543 syntax: cx.theme().syntax().clone(),
544 code_block: StyleRefinement {
545 text: TextStyleRefinement {
546 font_family: Some(
547 theme_settings.buffer_font.family.clone(),
548 ),
549 font_size: Some(buffer_font_size.into()),
550 ..Default::default()
551 },
552 padding: EdgesRefinement {
553 top: Some(DefiniteLength::Absolute(
554 AbsoluteLength::Pixels(px(8.)),
555 )),
556 left: Some(DefiniteLength::Absolute(
557 AbsoluteLength::Pixels(px(8.)),
558 )),
559 right: Some(DefiniteLength::Absolute(
560 AbsoluteLength::Pixels(px(8.)),
561 )),
562 bottom: Some(DefiniteLength::Absolute(
563 AbsoluteLength::Pixels(px(8.)),
564 )),
565 },
566 margin: EdgesRefinement {
567 top: Some(Length::Definite(px(8.).into())),
568 left: Some(Length::Definite(px(0.).into())),
569 right: Some(Length::Definite(px(0.).into())),
570 bottom: Some(Length::Definite(px(12.).into())),
571 },
572 border_style: Some(BorderStyle::Solid),
573 border_widths: EdgesRefinement {
574 top: Some(AbsoluteLength::Pixels(px(1.))),
575 left: Some(AbsoluteLength::Pixels(px(1.))),
576 right: Some(AbsoluteLength::Pixels(px(1.))),
577 bottom: Some(AbsoluteLength::Pixels(px(1.))),
578 },
579 border_color: Some(cx.theme().colors().border_variant),
580 background: Some(
581 cx.theme().colors().editor_background.into(),
582 ),
583 ..Default::default()
584 },
585 ..Default::default()
586 },
587 )
588 .into_any_element()
589 } else {
590 div()
591 .child("No active completion".to_string())
592 .into_any_element()
593 }),
594 )
595 .id("raw-input-view"),
596 )
597 }
598
599 fn render_active_completion(
600 &mut self,
601 window: &mut Window,
602 cx: &mut Context<Self>,
603 ) -> Option<impl IntoElement> {
604 let active_prediction = self.active_prediction.as_ref()?;
605 let completion_id = active_prediction.prediction.id.clone();
606 let focus_handle = &self.focus_handle(cx);
607
608 let border_color = cx.theme().colors().border;
609 let bg_color = cx.theme().colors().editor_background;
610
611 let rated = self.ep_store.read(cx).is_prediction_rated(&completion_id);
612 let feedback_empty = active_prediction
613 .feedback_editor
614 .read(cx)
615 .text(cx)
616 .is_empty();
617
618 let label_container = h_flex().pl_1().gap_1p5();
619
620 Some(
621 v_flex()
622 .size_full()
623 .overflow_hidden()
624 .relative()
625 .child(
626 v_flex()
627 .size_full()
628 .overflow_hidden()
629 .relative()
630 .child(self.render_view_nav(cx))
631 .when_some(
632 match self.current_view {
633 RatePredictionView::SuggestedEdits => {
634 self.render_suggested_edits(cx)
635 }
636 RatePredictionView::RawInput => self.render_raw_input(window, cx),
637 },
638 |this, element| this.child(element),
639 ),
640 )
641 .when(!rated, |this| {
642 let modal = cx.entity().downgrade();
643 let failure_mode_menu =
644 ContextMenu::build(window, cx, move |menu, _window, _cx| {
645 FeedbackCompletionProvider::FAILURE_MODES
646 .iter()
647 .fold(menu, |menu, (key, description)| {
648 let key: SharedString = (*key).into();
649 let description: SharedString = (*description).into();
650 let modal = modal.clone();
651 menu.entry(
652 format!("{} {}", key, description),
653 None,
654 move |window, cx| {
655 if let Some(modal) = modal.upgrade() {
656 modal.update(cx, |this, cx| {
657 if let Some(active) = &this.active_prediction {
658 active.feedback_editor.update(
659 cx,
660 |editor, cx| {
661 editor.set_text(
662 format!("{} {}", key, description),
663 window,
664 cx,
665 );
666 },
667 );
668 }
669 });
670 }
671 },
672 )
673 })
674 });
675
676 this.child(
677 h_flex()
678 .p_2()
679 .gap_2()
680 .border_y_1()
681 .border_color(border_color)
682 .child(
683 DropdownMenu::new(
684 "failure-mode-dropdown",
685 "Issue",
686 failure_mode_menu,
687 )
688 .handle(self.failure_mode_menu_handle.clone())
689 .style(ui::DropdownStyle::Outlined)
690 .trigger_size(ButtonSize::Compact),
691 )
692 .child(
693 h_flex()
694 .gap_2()
695 .child(
696 Icon::new(IconName::Info)
697 .size(IconSize::XSmall)
698 .color(Color::Muted),
699 )
700 .child(
701 div().flex_wrap().child(
702 Label::new(concat!(
703 "Explain why this completion is good or bad. ",
704 "If it's negative, describe what you expected instead."
705 ))
706 .size(LabelSize::Small)
707 .color(Color::Muted),
708 ),
709 ),
710 ),
711 )
712 })
713 .when(!rated, |this| {
714 this.child(
715 div()
716 .h_40()
717 .pt_1()
718 .bg(bg_color)
719 .child(active_prediction.feedback_editor.clone()),
720 )
721 })
722 .child(
723 h_flex()
724 .p_1()
725 .h_8()
726 .max_h_8()
727 .border_t_1()
728 .border_color(border_color)
729 .max_w_full()
730 .justify_between()
731 .children(if rated {
732 Some(
733 label_container
734 .child(
735 Icon::new(IconName::Check)
736 .size(IconSize::Small)
737 .color(Color::Success),
738 )
739 .child(Label::new("Rated completion.").color(Color::Muted)),
740 )
741 } else if active_prediction.prediction.edits.is_empty() {
742 Some(
743 label_container
744 .child(
745 Icon::new(IconName::Warning)
746 .size(IconSize::Small)
747 .color(Color::Warning),
748 )
749 .child(Label::new("No edits produced.").color(Color::Muted)),
750 )
751 } else {
752 Some(label_container)
753 })
754 .child(
755 h_flex()
756 .gap_1()
757 .child(
758 Button::new("bad", "Bad Prediction")
759 .start_icon(Icon::new(IconName::ThumbsDown).size(IconSize::Small))
760 .disabled(rated || feedback_empty)
761 .when(feedback_empty, |this| {
762 this.tooltip(Tooltip::text(
763 "Explain what's bad about it before reporting it",
764 ))
765 })
766 .key_binding(KeyBinding::for_action_in(
767 &ThumbsDownActivePrediction,
768 focus_handle,
769 cx,
770 ))
771 .on_click(cx.listener(move |this, _, window, cx| {
772 if this.active_prediction.is_some() {
773 this.thumbs_down_active(
774 &ThumbsDownActivePrediction,
775 window,
776 cx,
777 );
778 }
779 })),
780 )
781 .child(
782 Button::new("good", "Good Prediction")
783 .start_icon(Icon::new(IconName::ThumbsUp).size(IconSize::Small))
784 .disabled(rated)
785 .key_binding(KeyBinding::for_action_in(
786 &ThumbsUpActivePrediction,
787 focus_handle,
788 cx,
789 ))
790 .on_click(cx.listener(move |this, _, window, cx| {
791 if this.active_prediction.is_some() {
792 this.thumbs_up_active(
793 &ThumbsUpActivePrediction,
794 window,
795 cx,
796 );
797 }
798 })),
799 ),
800 ),
801 ),
802 )
803 }
804
805 fn render_shown_completions(&self, cx: &Context<Self>) -> impl Iterator<Item = ListItem> {
806 self.ep_store
807 .read(cx)
808 .shown_predictions()
809 .cloned()
810 .enumerate()
811 .map(|(index, completion)| {
812 let selected = self
813 .active_prediction
814 .as_ref()
815 .is_some_and(|selected| selected.prediction.id == completion.id);
816 let rated = self.ep_store.read(cx).is_prediction_rated(&completion.id);
817
818 let (icon_name, icon_color, tooltip_text) =
819 match (rated, completion.edits.is_empty()) {
820 (true, _) => (IconName::Check, Color::Success, "Rated Prediction"),
821 (false, true) => (IconName::File, Color::Muted, "No Edits Produced"),
822 (false, false) => (IconName::FileDiff, Color::Accent, "Edits Available"),
823 };
824
825 let file = completion.buffer.read(cx).file();
826 let file_name = file
827 .as_ref()
828 .map_or(SharedString::new_static("untitled"), |file| {
829 file.file_name(cx).to_string().into()
830 });
831 let file_path = file.map(|file| file.path().as_unix_str().to_string());
832
833 ListItem::new(completion.id.clone())
834 .inset(true)
835 .spacing(ListItemSpacing::Sparse)
836 .focused(index == self.selected_index)
837 .toggle_state(selected)
838 .child(
839 h_flex()
840 .id("completion-content")
841 .gap_3()
842 .child(Icon::new(icon_name).color(icon_color).size(IconSize::Small))
843 .child(
844 v_flex().child(
845 h_flex()
846 .gap_1()
847 .child(Label::new(file_name).size(LabelSize::Small))
848 .when_some(file_path, |this, p| {
849 this.child(
850 Label::new(p)
851 .size(LabelSize::Small)
852 .color(Color::Muted),
853 )
854 }),
855 ),
856 ),
857 )
858 .tooltip(Tooltip::text(tooltip_text))
859 .on_click(cx.listener(move |this, _, window, cx| {
860 this.select_completion(Some(completion.clone()), true, window, cx);
861 }))
862 })
863 }
864}
865
866impl Render for RatePredictionsModal {
867 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
868 let border_color = cx.theme().colors().border;
869
870 h_flex()
871 .key_context("RatePredictionModal")
872 .track_focus(&self.focus_handle)
873 .on_action(cx.listener(Self::dismiss))
874 .on_action(cx.listener(Self::confirm))
875 .on_action(cx.listener(Self::select_previous))
876 .on_action(cx.listener(Self::select_prev_edit))
877 .on_action(cx.listener(Self::select_next))
878 .on_action(cx.listener(Self::select_next_edit))
879 .on_action(cx.listener(Self::select_first))
880 .on_action(cx.listener(Self::select_last))
881 .on_action(cx.listener(Self::thumbs_up_active))
882 .on_action(cx.listener(Self::thumbs_down_active))
883 .on_action(cx.listener(Self::focus_completions))
884 .on_action(cx.listener(Self::preview_completion))
885 .bg(cx.theme().colors().elevated_surface_background)
886 .border_1()
887 .border_color(border_color)
888 .w(window.viewport_size().width - px(320.))
889 .h(window.viewport_size().height - px(300.))
890 .rounded_lg()
891 .shadow_lg()
892 .child(
893 v_flex()
894 .w_72()
895 .h_full()
896 .border_r_1()
897 .border_color(border_color)
898 .flex_shrink_0()
899 .overflow_hidden()
900 .child({
901 let icons = self.ep_store.read(cx).icons(cx);
902 h_flex()
903 .h_8()
904 .px_2()
905 .justify_between()
906 .border_b_1()
907 .border_color(border_color)
908 .child(Icon::new(icons.base).size(IconSize::Small))
909 .child(
910 Label::new("From most recent to oldest")
911 .color(Color::Muted)
912 .size(LabelSize::Small),
913 )
914 })
915 .child(
916 div()
917 .id("completion_list")
918 .p_0p5()
919 .h_full()
920 .overflow_y_scroll()
921 .child(
922 List::new()
923 .empty_message(
924 div()
925 .p_2()
926 .child(
927 Label::new(concat!(
928 "No completions yet. ",
929 "Use the editor to generate some, ",
930 "and make sure to rate them!"
931 ))
932 .color(Color::Muted),
933 )
934 .into_any_element(),
935 )
936 .children(self.render_shown_completions(cx)),
937 ),
938 ),
939 )
940 .children(self.render_active_completion(window, cx))
941 .on_mouse_down_out(cx.listener(|this, _, _, cx| {
942 if !this.failure_mode_menu_handle.is_deployed() {
943 cx.emit(DismissEvent);
944 }
945 }))
946 }
947}
948
949impl EventEmitter<DismissEvent> for RatePredictionsModal {}
950
951impl Focusable for RatePredictionsModal {
952 fn focus_handle(&self, _cx: &App) -> FocusHandle {
953 self.focus_handle.clone()
954 }
955}
956
957impl ModalView for RatePredictionsModal {}
958
959struct FeedbackCompletionProvider;
960
961impl FeedbackCompletionProvider {
962 const FAILURE_MODES: &'static [(&'static str, &'static str)] = &[
963 ("@location", "Unexpected location"),
964 ("@malformed", "Incomplete, cut off, or syntax error"),
965 (
966 "@deleted",
967 "Deleted code that should be kept (use `@reverted` if it undid a recent edit)",
968 ),
969 ("@style", "Wrong coding style or conventions"),
970 ("@repetitive", "Repeated existing code"),
971 ("@hallucinated", "Referenced non-existent symbols"),
972 ("@formatting", "Wrong indentation or structure"),
973 ("@aggressive", "Changed more than expected"),
974 ("@conservative", "Too cautious, changed too little"),
975 ("@context", "Ignored or misunderstood context"),
976 ("@reverted", "Undid recent edits"),
977 ("@cursor_position", "Cursor placed in unhelpful position"),
978 ("@whitespace", "Unwanted whitespace or newline changes"),
979 ];
980}
981
982impl editor::CompletionProvider for FeedbackCompletionProvider {
983 fn completions(
984 &self,
985 buffer: &Entity<Buffer>,
986 buffer_position: language::Anchor,
987 _trigger: editor::CompletionContext,
988 _window: &mut Window,
989 cx: &mut Context<Editor>,
990 ) -> gpui::Task<anyhow::Result<Vec<CompletionResponse>>> {
991 let buffer = buffer.read(cx);
992 let mut count_back = 0;
993
994 for char in buffer.reversed_chars_at(buffer_position) {
995 if char.is_ascii_alphanumeric() || char == '_' || char == '@' {
996 count_back += 1;
997 } else {
998 break;
999 }
1000 }
1001
1002 let start_anchor = buffer.anchor_before(
1003 buffer_position
1004 .to_offset(&buffer)
1005 .saturating_sub(count_back),
1006 );
1007
1008 let replace_range = start_anchor..buffer_position;
1009 let snapshot = buffer.text_snapshot();
1010 let query: String = snapshot.text_for_range(replace_range.clone()).collect();
1011
1012 if !query.starts_with('@') {
1013 return gpui::Task::ready(Ok(vec![CompletionResponse {
1014 completions: vec![],
1015 display_options: CompletionDisplayOptions {
1016 dynamic_width: true,
1017 },
1018 is_incomplete: false,
1019 }]));
1020 }
1021
1022 let query_lower = query.to_lowercase();
1023
1024 let completions: Vec<Completion> = Self::FAILURE_MODES
1025 .iter()
1026 .filter(|(key, _description)| key.starts_with(&query_lower))
1027 .map(|(key, description)| Completion {
1028 replace_range: replace_range.clone(),
1029 new_text: format!("{} {}", key, description),
1030 label: CodeLabel::plain(format!("{}: {}", key, description), None),
1031 documentation: None,
1032 source: CompletionSource::Custom,
1033 icon_path: None,
1034 match_start: None,
1035 snippet_deduplication_key: None,
1036 insert_text_mode: None,
1037 confirm: None,
1038 })
1039 .collect();
1040
1041 gpui::Task::ready(Ok(vec![CompletionResponse {
1042 completions,
1043 display_options: CompletionDisplayOptions {
1044 dynamic_width: true,
1045 },
1046 is_incomplete: false,
1047 }]))
1048 }
1049
1050 fn is_completion_trigger(
1051 &self,
1052 _buffer: &Entity<Buffer>,
1053 _position: language::Anchor,
1054 text: &str,
1055 _trigger_in_words: bool,
1056 _cx: &mut Context<Editor>,
1057 ) -> bool {
1058 text.chars()
1059 .last()
1060 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_' || c == '@')
1061 }
1062}