Skip to repository content1514 lines · 63.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:00:44.394Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
rate_prediction_modal.rs
1use buffer_diff::BufferDiff;
2use cloud_llm_client::PredictEditsRequestTrigger;
3use edit_prediction::{
4 EditPrediction, EditPredictionInputs, EditPredictionRating, EditPredictionStore,
5};
6use editor::{Editor, Inlay, MultiBuffer};
7use feature_flags::{FeatureFlag, PresenceFlag, register_feature_flag};
8use gpui::{
9 App, BorderStyle, DismissEvent, EdgesRefinement, Entity, EventEmitter, FocusHandle, Focusable,
10 Length, StyleRefinement, Task, TextStyleRefinement, Window, actions, prelude::*,
11};
12use language::{
13 Bias, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, Point, ToOffset, ToPoint,
14 language_settings::{self, InlayHintKind},
15};
16use markdown::{Markdown, MarkdownStyle};
17use project::{
18 Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, InlayHint,
19 InlayHintLabel, InlayId, ResolveState,
20};
21use settings::Settings as _;
22use std::rc::Rc;
23use std::{fmt::Write, ops::Range, path::Path, sync::Arc};
24use theme_settings::ThemeSettings;
25use ui::{
26 ContextMenu, DropdownMenu, KeyBinding, List, ListItem, ListItemSpacing, PopoverMenuHandle,
27 Tooltip, prelude::*,
28};
29use workspace::{ModalView, Workspace};
30use zeta_prompt::{ContextSource, FilePosition, RelatedExcerpt, RelatedFile, Zeta3PromptInput};
31
32actions!(
33 zeta,
34 [
35 /// Rates the active completion with a thumbs up.
36 ThumbsUpActivePrediction,
37 /// Rates the active completion with a thumbs down.
38 ThumbsDownActivePrediction,
39 /// Navigates to the next edit in the completion history.
40 NextEdit,
41 /// Navigates to the previous edit in the completion history.
42 PreviousEdit,
43 /// Focuses on the completions list.
44 FocusPredictions,
45 /// Previews the selected completion.
46 PreviewPrediction,
47 ]
48);
49
50pub struct PredictEditsRatePredictionsFeatureFlag;
51
52impl FeatureFlag for PredictEditsRatePredictionsFeatureFlag {
53 const NAME: &'static str = "predict-edits-rate-completions";
54 type Value = PresenceFlag;
55}
56register_feature_flag!(PredictEditsRatePredictionsFeatureFlag);
57
58pub struct RatePredictionsModal {
59 ep_store: Entity<EditPredictionStore>,
60 language_registry: Arc<LanguageRegistry>,
61 active_prediction: Option<ActivePrediction>,
62 selected_index: usize,
63 diff_editor: Entity<Editor>,
64 focus_handle: FocusHandle,
65 _subscription: gpui::Subscription,
66 current_view: RatePredictionView,
67 failure_mode_menu_handle: PopoverMenuHandle<ContextMenu>,
68}
69
70struct ActivePrediction {
71 prediction: EditPrediction,
72 feedback_editor: Entity<Editor>,
73 expected_buffer: Entity<Buffer>,
74 expected_editor: Entity<Editor>,
75 _expected_buffer_subscription: gpui::Subscription,
76 formatted_inputs: Entity<Markdown>,
77 _predicted_diff_task: Task<()>,
78 expected_diff_task: Task<()>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
82enum RatePredictionView {
83 SuggestedEdits,
84 RawInput,
85}
86
87impl RatePredictionView {
88 pub fn name(&self) -> &'static str {
89 match self {
90 Self::SuggestedEdits => "Suggested Edits",
91 Self::RawInput => "Recorded Events & Input",
92 }
93 }
94}
95
96impl RatePredictionsModal {
97 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
98 if let Some(ep_store) = EditPredictionStore::try_global(cx) {
99 let language_registry = workspace.app_state().languages.clone();
100 workspace.toggle_modal(window, cx, |window, cx| {
101 RatePredictionsModal::new(ep_store, language_registry, window, cx)
102 });
103
104 telemetry::event!("Rate Prediction Modal Open", source = "Edit Prediction");
105 }
106 }
107
108 pub fn new(
109 ep_store: Entity<EditPredictionStore>,
110 language_registry: Arc<LanguageRegistry>,
111 window: &mut Window,
112 cx: &mut Context<Self>,
113 ) -> Self {
114 let subscription = cx.observe(&ep_store, |_, _, cx| cx.notify());
115
116 Self {
117 ep_store,
118 language_registry,
119 selected_index: 0,
120 focus_handle: cx.focus_handle(),
121 active_prediction: None,
122 _subscription: subscription,
123 diff_editor: cx.new(|cx| {
124 let multibuffer = cx.new(|_| MultiBuffer::new(language::Capability::ReadOnly));
125 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
126 editor.disable_inline_diagnostics();
127 editor.set_expand_all_diff_hunks(cx);
128 editor.set_show_git_diff_gutter(false, cx);
129 editor
130 }),
131 current_view: RatePredictionView::SuggestedEdits,
132 failure_mode_menu_handle: PopoverMenuHandle::default(),
133 }
134 }
135
136 fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
137 cx.emit(DismissEvent);
138 }
139
140 fn select_next(&mut self, _: &menu::SelectNext, _: &mut Window, cx: &mut Context<Self>) {
141 self.selected_index += 1;
142 self.selected_index = usize::min(
143 self.selected_index,
144 self.ep_store.read(cx).rateable_predictions().count(),
145 );
146 cx.notify();
147 }
148
149 fn select_previous(
150 &mut self,
151 _: &menu::SelectPrevious,
152 _: &mut Window,
153 cx: &mut Context<Self>,
154 ) {
155 self.selected_index = self.selected_index.saturating_sub(1);
156 cx.notify();
157 }
158
159 fn select_next_edit(&mut self, _: &NextEdit, _: &mut Window, cx: &mut Context<Self>) {
160 let next_index = self
161 .ep_store
162 .read(cx)
163 .rateable_predictions()
164 .skip(self.selected_index)
165 .enumerate()
166 .skip(1) // Skip straight to the next item
167 .find(|(_, completion)| !completion.edits.is_empty())
168 .map(|(ix, _)| ix + self.selected_index);
169
170 if let Some(next_index) = next_index {
171 self.selected_index = next_index;
172 cx.notify();
173 }
174 }
175
176 fn select_prev_edit(&mut self, _: &PreviousEdit, _: &mut Window, cx: &mut Context<Self>) {
177 let ep_store = self.ep_store.read(cx);
178 let completions_len = ep_store.rateable_predictions_count();
179
180 let prev_index = self
181 .ep_store
182 .read(cx)
183 .rateable_predictions()
184 .rev()
185 .skip((completions_len - 1) - self.selected_index)
186 .enumerate()
187 .skip(1) // Skip straight to the previous item
188 .find(|(_, completion)| !completion.edits.is_empty())
189 .map(|(ix, _)| self.selected_index - ix);
190
191 if let Some(prev_index) = prev_index {
192 self.selected_index = prev_index;
193 cx.notify();
194 }
195 cx.notify();
196 }
197
198 fn select_first(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
199 self.selected_index = 0;
200 cx.notify();
201 }
202
203 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
204 self.selected_index = self.ep_store.read(cx).rateable_predictions_count() - 1;
205 cx.notify();
206 }
207
208 pub fn thumbs_up_active(
209 &mut self,
210 _: &ThumbsUpActivePrediction,
211 window: &mut Window,
212 cx: &mut Context<Self>,
213 ) {
214 self.ep_store.update(cx, |ep_store, cx| {
215 if let Some(active) = &self.active_prediction {
216 ep_store.rate_prediction(
217 &active.prediction,
218 EditPredictionRating::Positive,
219 active.feedback_editor.read(cx).text(cx),
220 self.expected_patch_for_active(cx),
221 cx,
222 );
223 }
224 });
225
226 let current_completion = self
227 .active_prediction
228 .as_ref()
229 .map(|completion| completion.prediction.clone());
230 self.select_completion(current_completion, false, window, cx);
231 self.select_next_edit(&Default::default(), window, cx);
232 self.confirm(&Default::default(), window, cx);
233
234 cx.notify();
235 }
236
237 pub fn thumbs_down_active(
238 &mut self,
239 _: &ThumbsDownActivePrediction,
240 window: &mut Window,
241 cx: &mut Context<Self>,
242 ) {
243 if let Some(active) = &self.active_prediction {
244 if active.feedback_editor.read(cx).text(cx).is_empty() {
245 return;
246 }
247
248 self.ep_store.update(cx, |ep_store, cx| {
249 ep_store.rate_prediction(
250 &active.prediction,
251 EditPredictionRating::Negative,
252 active.feedback_editor.read(cx).text(cx),
253 self.expected_patch_for_active(cx),
254 cx,
255 );
256 });
257 }
258
259 let current_completion = self
260 .active_prediction
261 .as_ref()
262 .map(|completion| completion.prediction.clone());
263 self.select_completion(current_completion, false, window, cx);
264 self.select_next_edit(&Default::default(), window, cx);
265 self.confirm(&Default::default(), window, cx);
266
267 cx.notify();
268 }
269
270 fn focus_completions(
271 &mut self,
272 _: &FocusPredictions,
273 window: &mut Window,
274 cx: &mut Context<Self>,
275 ) {
276 cx.focus_self(window);
277 cx.notify();
278 }
279
280 fn preview_completion(
281 &mut self,
282 _: &PreviewPrediction,
283 window: &mut Window,
284 cx: &mut Context<Self>,
285 ) {
286 let completion = self
287 .ep_store
288 .read(cx)
289 .rateable_predictions()
290 .skip(self.selected_index)
291 .take(1)
292 .next()
293 .cloned();
294
295 self.select_completion(completion, false, window, cx);
296 }
297
298 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
299 let completion = self
300 .ep_store
301 .read(cx)
302 .rateable_predictions()
303 .skip(self.selected_index)
304 .take(1)
305 .next()
306 .cloned();
307
308 self.select_completion(completion, true, window, cx);
309 }
310
311 fn update_buffer_diff(
312 diff: &Entity<BufferDiff>,
313 new_buffer_snapshot: BufferSnapshot,
314 old_buffer_snapshot: BufferSnapshot,
315 cx: &mut App,
316 ) -> Task<()> {
317 diff.update(cx, |diff, cx| {
318 diff.set_base_text(
319 Some(old_buffer_snapshot.text().into()),
320 new_buffer_snapshot.text,
321 cx,
322 )
323 })
324 }
325
326 fn insert_editable_region_markers(
327 editor: &Entity<Editor>,
328 buffer: &Entity<Buffer>,
329 marker_range: Range<usize>,
330 cx: &mut Context<Self>,
331 ) {
332 editor.update(cx, |editor, cx| {
333 let buffer_snapshot = buffer.read(cx).snapshot();
334 let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx);
335 let start_buffer_anchor = buffer_snapshot
336 .anchor_after(buffer_snapshot.clip_offset(marker_range.start, Bias::Left));
337 let end_buffer_anchor = buffer_snapshot
338 .anchor_after(buffer_snapshot.clip_offset(marker_range.end, Bias::Right));
339 let Some(start_anchor) = multibuffer_snapshot.anchor_in_excerpt(start_buffer_anchor)
340 else {
341 return;
342 };
343 let Some(end_anchor) = multibuffer_snapshot.anchor_in_excerpt(end_buffer_anchor) else {
344 return;
345 };
346 let Some((start_hint_position, _)) =
347 multibuffer_snapshot.anchor_to_buffer_anchor(start_anchor)
348 else {
349 return;
350 };
351 let Some((end_hint_position, _)) =
352 multibuffer_snapshot.anchor_to_buffer_anchor(end_anchor)
353 else {
354 return;
355 };
356
357 editor.splice_inlays(
358 &[InlayId::Hint(0), InlayId::Hint(1)],
359 vec![
360 Inlay::hint(
361 InlayId::Hint(0),
362 start_anchor,
363 &InlayHint {
364 position: start_hint_position,
365 label: InlayHintLabel::String("╭─ editable region start\n".into()),
366 kind: Some(InlayHintKind::Parameter),
367 padding_left: false,
368 padding_right: false,
369 tooltip: None,
370 resolve_state: ResolveState::Resolved,
371 },
372 ),
373 Inlay::hint(
374 InlayId::Hint(1),
375 end_anchor,
376 &InlayHint {
377 position: end_hint_position,
378 label: InlayHintLabel::String("\n╰─ editable region end".into()),
379 kind: Some(InlayHintKind::Parameter),
380 padding_left: false,
381 padding_right: false,
382 tooltip: None,
383 resolve_state: ResolveState::Resolved,
384 },
385 ),
386 ],
387 cx,
388 );
389 });
390 }
391
392 fn expected_patch_for_active(&self, cx: &App) -> Option<String> {
393 let active_prediction = self.active_prediction.as_ref()?;
394 let expected_text = active_prediction.expected_buffer.read(cx).snapshot().text();
395 let original_text = active_prediction.prediction.snapshot.text();
396 let diff_body = language::unified_diff(&original_text, &expected_text);
397
398 if diff_body.is_empty() {
399 return None;
400 }
401
402 let path = active_prediction
403 .prediction
404 .snapshot
405 .file()
406 .map(|file| file.path().as_unix_str());
407 let header = match path {
408 Some(path) => format!("--- a/{path}\n+++ b/{path}\n"),
409 None => String::new(),
410 };
411
412 Some(format!("{header}{diff_body}"))
413 }
414
415 fn write_formatted_inputs(formatted_inputs: &mut String, inputs: &EditPredictionInputs) {
416 match inputs {
417 EditPredictionInputs::V2(inputs) => {
418 Self::write_events(formatted_inputs, &inputs.events);
419 Self::write_related_files(
420 formatted_inputs,
421 inputs.related_files.as_deref().unwrap_or_default(),
422 );
423 Self::write_cursor_excerpt(
424 formatted_inputs,
425 inputs.cursor_path.as_ref(),
426 inputs.cursor_excerpt.as_ref(),
427 inputs.cursor_offset_in_excerpt,
428 );
429 }
430 EditPredictionInputs::V3(inputs) => {
431 Self::write_events(formatted_inputs, &inputs.events);
432 Self::write_related_files(formatted_inputs, &inputs.editable_context);
433 Self::write_zeta3_cursor_excerpt(formatted_inputs, inputs);
434 }
435 }
436 }
437
438 fn write_events(formatted_inputs: &mut String, events: &[Arc<zeta_prompt::Event>]) {
439 write!(formatted_inputs, "## Events\n\n").unwrap();
440
441 for event in events {
442 formatted_inputs.push_str("```diff\n");
443 zeta_prompt::write_event(formatted_inputs, event.as_ref());
444 formatted_inputs.push_str("```\n\n");
445 }
446 }
447
448 fn write_related_files(formatted_inputs: &mut String, included_files: &[RelatedFile]) {
449 write!(formatted_inputs, "## Related files\n\n").unwrap();
450
451 for included_file in included_files {
452 write!(formatted_inputs, "### {}\n\n", included_file.path.display()).unwrap();
453
454 for excerpt in included_file.excerpts.iter() {
455 write!(
456 formatted_inputs,
457 "```{}\n{}\n```\n",
458 included_file.path.display(),
459 excerpt.text
460 )
461 .unwrap();
462 }
463 }
464 }
465
466 fn write_zeta3_cursor_excerpt(formatted_inputs: &mut String, inputs: &Zeta3PromptInput) {
467 let current_excerpt = inputs
468 .editable_context
469 .iter()
470 .filter(|file| file.path == inputs.cursor_path)
471 .flat_map(|file| file.excerpts.iter())
472 .find_map(|excerpt| {
473 if excerpt.context_source != ContextSource::CurrentFile {
474 return None;
475 }
476
477 Some((
478 excerpt,
479 Self::offset_for_position_in_excerpt(excerpt, inputs.cursor_position)?,
480 ))
481 });
482
483 if let Some((excerpt, cursor_offset)) = current_excerpt {
484 Self::write_cursor_excerpt(
485 formatted_inputs,
486 inputs.cursor_path.as_ref(),
487 excerpt.text.as_ref(),
488 cursor_offset,
489 );
490 } else {
491 write!(formatted_inputs, "## Cursor Excerpt\n\n").unwrap();
492 writeln!(
493 formatted_inputs,
494 "No current-file excerpt found for `{}` at row {}, column {}.",
495 inputs.cursor_path.display(),
496 inputs.cursor_position.row,
497 inputs.cursor_position.column
498 )
499 .unwrap();
500 }
501 }
502
503 fn write_cursor_excerpt(
504 formatted_inputs: &mut String,
505 cursor_path: &Path,
506 cursor_excerpt: &str,
507 cursor_offset: usize,
508 ) {
509 write!(formatted_inputs, "## Cursor Excerpt\n\n").unwrap();
510
511 let mut cursor_offset = cursor_offset.min(cursor_excerpt.len());
512 while !cursor_excerpt.is_char_boundary(cursor_offset) {
513 cursor_offset = cursor_offset.saturating_sub(1);
514 }
515 writeln!(
516 formatted_inputs,
517 "```{}\n{}<CURSOR>{}\n```\n",
518 cursor_path.display(),
519 &cursor_excerpt[..cursor_offset],
520 &cursor_excerpt[cursor_offset..],
521 )
522 .unwrap();
523 }
524
525 fn offset_for_position_in_excerpt(
526 excerpt: &RelatedExcerpt,
527 position: FilePosition,
528 ) -> Option<usize> {
529 if position.row < excerpt.row_range.start {
530 return None;
531 }
532
533 let relative_row = (position.row - excerpt.row_range.start) as usize;
534 let text = excerpt.text.as_ref();
535 let mut row_start = 0;
536
537 for row in 0..=relative_row {
538 if row == relative_row {
539 let row_end = text[row_start..]
540 .find('\n')
541 .map_or(text.len(), |offset| row_start + offset);
542 let row_text = &text[row_start..row_end];
543 let column =
544 row_text.floor_char_boundary((position.column as usize).min(row_text.len()));
545 return Some(row_start + column);
546 }
547
548 row_start += text[row_start..].find('\n')? + 1;
549 }
550
551 None
552 }
553
554 pub fn select_completion(
555 &mut self,
556 prediction: Option<EditPrediction>,
557 focus: bool,
558 window: &mut Window,
559 cx: &mut Context<Self>,
560 ) {
561 // Avoid resetting completion rating if it's already selected.
562 if let Some(prediction) = prediction {
563 self.selected_index = self
564 .ep_store
565 .read(cx)
566 .rateable_predictions()
567 .enumerate()
568 .find(|(_, completion_b)| prediction.id == completion_b.id)
569 .map(|(ix, _)| ix)
570 .unwrap_or(self.selected_index);
571 cx.notify();
572
573 if let Some(prev_prediction) = self.active_prediction.as_ref()
574 && prediction.id == prev_prediction.prediction.id
575 {
576 if focus {
577 window.focus(&prev_prediction.feedback_editor.focus_handle(cx), cx);
578 }
579 return;
580 }
581
582 let editable_range = prediction.editable_range.clone().or_else(|| {
583 Some(prediction.edits.first()?.0.start..prediction.edits.last()?.0.end)
584 });
585 let predicted_buffer = prediction.edit_preview.build_result_buffer(cx);
586 let predicted_buffer_snapshot = predicted_buffer.read(cx).snapshot();
587 let visible_range = prediction
588 .edit_preview
589 .compute_visible_range(&prediction.edits)
590 .or_else(|| {
591 editable_range.as_ref().map(|range| {
592 range.start.to_point(&prediction.snapshot)
593 ..range.end.to_point(&prediction.snapshot)
594 })
595 })
596 .unwrap_or(Point::zero()..Point::zero());
597 let visible_range_with_context =
598 Point::new(visible_range.start.row.saturating_sub(5), 0)
599 ..Point::new(visible_range.end.row.saturating_add(5), 0)
600 .min(predicted_buffer_snapshot.max_point());
601 let predicted_diff_task = self.diff_editor.update(cx, |editor, cx| {
602 let predicted_buffer_id = predicted_buffer_snapshot.remote_id();
603 let diff = cx.new(|cx| {
604 BufferDiff::new(
605 &predicted_buffer_snapshot.text,
606 predicted_buffer_snapshot.language().cloned(),
607 predicted_buffer.read(cx).language_registry(),
608 cx,
609 )
610 });
611 let predicted_diff_task = Self::update_buffer_diff(
612 &diff,
613 predicted_buffer_snapshot.clone(),
614 prediction.snapshot.clone(),
615 cx,
616 );
617
618 editor.disable_header_for_buffer(predicted_buffer_id, cx);
619 editor.buffer().update(cx, |multibuffer, cx| {
620 multibuffer.clear(cx);
621 multibuffer.set_excerpts_for_buffer(
622 predicted_buffer.clone(),
623 [visible_range_with_context],
624 0,
625 cx,
626 );
627 multibuffer.add_diff(diff, cx);
628 });
629 predicted_diff_task
630 });
631
632 if let Some(editable_range) = editable_range.as_ref() {
633 Self::insert_editable_region_markers(
634 &self.diff_editor,
635 &predicted_buffer,
636 prediction
637 .edit_preview
638 .anchor_to_offset_in_result(editable_range.start)
639 ..prediction
640 .edit_preview
641 .anchor_to_offset_in_result(editable_range.end),
642 cx,
643 );
644 }
645
646 self.diff_editor.update(cx, |editor, cx| {
647 if let Some(cursor_position) = prediction.cursor_position.as_ref() {
648 let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx);
649 let cursor_offset = prediction
650 .edit_preview
651 .anchor_to_offset_in_result(cursor_position.anchor)
652 + cursor_position.offset;
653 let predicted_buffer_snapshot = predicted_buffer.read(cx).snapshot();
654 let cursor_anchor = predicted_buffer_snapshot.anchor_after(
655 predicted_buffer_snapshot.clip_offset(cursor_offset, Bias::Right),
656 );
657
658 if let Some(anchor) = multibuffer_snapshot.anchor_in_excerpt(cursor_anchor) {
659 editor.splice_inlays(
660 &[InlayId::EditPrediction(0)],
661 vec![Inlay::edit_prediction(0, anchor, "▏")],
662 cx,
663 );
664 }
665 }
666 });
667
668 let mut formatted_inputs = String::new();
669 Self::write_formatted_inputs(&mut formatted_inputs, &prediction.inputs);
670
671 let current_editable_region = editable_range.as_ref().map(|range| {
672 prediction
673 .buffer
674 .read(cx)
675 .snapshot()
676 .text_for_range(range.clone())
677 .collect::<String>()
678 });
679 let expected_buffer = cx.new(|cx| {
680 let mut buffer = Buffer::local(prediction.snapshot.text(), cx);
681 buffer.set_language_async(prediction.snapshot.language().cloned(), cx);
682 buffer
683 });
684 let expected_editable_range = editable_range.as_ref().map(|editable_range| {
685 expected_buffer.update(cx, |buffer, cx| {
686 let snapshot = buffer.snapshot();
687 let editable_point_range = editable_range.start.to_point(&prediction.snapshot)
688 ..editable_range.end.to_point(&prediction.snapshot);
689 let expected_editable_range = snapshot.anchor_before(editable_point_range.start)
690 ..snapshot.anchor_after(editable_point_range.end);
691 if let Some(current_editable_region) = current_editable_region {
692 buffer.edit(
693 [(expected_editable_range.clone(), current_editable_region)],
694 None,
695 cx,
696 );
697 }
698 expected_editable_range
699 })
700 });
701 let expected_buffer_snapshot = expected_buffer.read(cx).snapshot();
702 let expected_excerpt_range = expected_editable_range
703 .as_ref()
704 .map(|range| {
705 range.start.to_point(&expected_buffer_snapshot)
706 ..range.end.to_point(&expected_buffer_snapshot)
707 })
708 .unwrap_or(visible_range);
709 let expected_diff = cx.new(|cx| {
710 BufferDiff::new(
711 &expected_buffer_snapshot.text,
712 expected_buffer_snapshot.language().cloned(),
713 expected_buffer.read(cx).language_registry(),
714 cx,
715 )
716 });
717 let expected_diff_task = Self::update_buffer_diff(
718 &expected_diff,
719 expected_buffer_snapshot.clone(),
720 prediction.snapshot.clone(),
721 cx,
722 );
723 let expected_editor = cx.new(|cx| {
724 let multibuffer = cx.new(|cx| {
725 let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
726 multibuffer.set_excerpts_for_buffer(
727 expected_buffer.clone(),
728 [expected_excerpt_range],
729 0,
730 cx,
731 );
732 multibuffer.add_diff(expected_diff.clone(), cx);
733 multibuffer
734 });
735 let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
736 let expected_buffer_id = expected_buffer.read(cx).remote_id();
737 editor.disable_header_for_buffer(expected_buffer_id, cx);
738 editor.disable_inline_diagnostics();
739 editor.set_expand_all_diff_hunks(cx);
740 editor.set_show_git_diff_gutter(false, cx);
741 editor.set_show_code_actions(false, cx);
742 editor.set_show_runnables(false, cx);
743 editor.set_show_bookmarks(false, cx);
744 editor.set_show_breakpoints(false, cx);
745 editor.set_show_wrap_guides(false, cx);
746 editor.set_show_edit_predictions(Some(false), window, cx);
747 editor
748 });
749 if let Some(expected_editable_range) = expected_editable_range.as_ref() {
750 let expected_buffer_snapshot = expected_buffer.read(cx).snapshot();
751 Self::insert_editable_region_markers(
752 &expected_editor,
753 &expected_buffer,
754 expected_editable_range
755 .start
756 .to_offset(&expected_buffer_snapshot)
757 ..expected_editable_range
758 .end
759 .to_offset(&expected_buffer_snapshot),
760 cx,
761 );
762 }
763
764 let expected_buffer_subscription = cx.subscribe(&expected_buffer, {
765 let expected_diff = expected_diff.clone();
766 let original_snapshot = prediction.snapshot.clone();
767 move |this, buffer, event, cx| match event {
768 language::BufferEvent::Edited { .. }
769 | language::BufferEvent::LanguageChanged(_)
770 | language::BufferEvent::Reparsed => {
771 let task = Self::update_buffer_diff(
772 &expected_diff,
773 buffer.read(cx).snapshot(),
774 original_snapshot.clone(),
775 cx,
776 );
777 if let Some(active_prediction) = this.active_prediction.as_mut() {
778 active_prediction.expected_diff_task = task;
779 }
780 }
781 _ => {}
782 }
783 });
784
785 self.active_prediction = Some(ActivePrediction {
786 prediction,
787 feedback_editor: cx.new(|cx| {
788 let mut editor = Editor::multi_line(window, cx);
789 editor.disable_scrollbars_and_minimap(window, cx);
790 editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
791 editor.set_show_line_numbers(false, cx);
792 editor.set_show_git_diff_gutter(false, cx);
793 editor.set_show_code_actions(false, cx);
794 editor.set_show_runnables(false, cx);
795 editor.set_show_bookmarks(false, cx);
796 editor.set_show_breakpoints(false, cx);
797 editor.set_show_wrap_guides(false, cx);
798 editor.set_show_indent_guides(false, cx);
799 editor.set_show_edit_predictions(Some(false), window, cx);
800 editor.set_placeholder_text("Add your feedback…", window, cx);
801 editor.set_completion_provider(Some(Rc::new(FeedbackCompletionProvider)));
802 if focus {
803 cx.focus_self(window);
804 }
805 editor
806 }),
807 expected_buffer,
808 expected_editor,
809 _expected_buffer_subscription: expected_buffer_subscription,
810 _predicted_diff_task: predicted_diff_task,
811 expected_diff_task,
812 formatted_inputs: cx.new(|cx| {
813 Markdown::new(
814 formatted_inputs.into(),
815 Some(self.language_registry.clone()),
816 None,
817 cx,
818 )
819 }),
820 });
821 } else {
822 self.active_prediction = None;
823 }
824
825 cx.notify();
826 }
827
828 fn render_view_nav(&self, cx: &Context<Self>) -> impl IntoElement {
829 h_flex()
830 .h_8()
831 .px_1()
832 .border_b_1()
833 .border_color(cx.theme().colors().border)
834 .bg(cx.theme().colors().elevated_surface_background)
835 .gap_1()
836 .child(
837 Button::new(
838 ElementId::Name("suggested-edits".into()),
839 RatePredictionView::SuggestedEdits.name(),
840 )
841 .label_size(LabelSize::Small)
842 .on_click(cx.listener(move |this, _, _window, cx| {
843 this.current_view = RatePredictionView::SuggestedEdits;
844 cx.notify();
845 }))
846 .toggle_state(self.current_view == RatePredictionView::SuggestedEdits),
847 )
848 .child(
849 Button::new(
850 ElementId::Name("raw-input".into()),
851 RatePredictionView::RawInput.name(),
852 )
853 .label_size(LabelSize::Small)
854 .on_click(cx.listener(move |this, _, _window, cx| {
855 this.current_view = RatePredictionView::RawInput;
856 cx.notify();
857 }))
858 .toggle_state(self.current_view == RatePredictionView::RawInput),
859 )
860 }
861
862 fn render_suggested_edits(&self, cx: &mut Context<Self>) -> Option<gpui::Stateful<Div>> {
863 let bg_color = cx.theme().colors().editor_background;
864 let border_color = cx.theme().colors().border;
865 let active_prediction = self.active_prediction.as_ref()?;
866
867 Some(
868 v_flex()
869 .id("diff")
870 .size_full()
871 .bg(bg_color)
872 .overflow_hidden()
873 .child(
874 v_flex()
875 .flex_1()
876 .min_h_0()
877 .child(
878 h_flex()
879 .h_8()
880 .px_2()
881 .border_b_1()
882 .border_color(border_color)
883 .child(Label::new("Predicted Patch").size(LabelSize::Small)),
884 )
885 .child(
886 div()
887 .id("predicted-patch-diff")
888 .p_4()
889 .flex_1()
890 .min_h_0()
891 .overflow_scroll()
892 .whitespace_nowrap()
893 .child(self.diff_editor.clone()),
894 ),
895 )
896 .child(
897 v_flex()
898 .flex_1()
899 .min_h_0()
900 .border_t_1()
901 .border_color(border_color)
902 .child(
903 h_flex()
904 .h_8()
905 .px_2()
906 .gap_2()
907 .border_b_1()
908 .border_color(border_color)
909 .child(Label::new("Expected Patch").size(LabelSize::Small)),
910 )
911 .child(
912 div()
913 .id("expected-patch")
914 .p_4()
915 .flex_1()
916 .min_h_0()
917 .overflow_scroll()
918 .whitespace_nowrap()
919 .child(active_prediction.expected_editor.clone()),
920 ),
921 ),
922 )
923 }
924
925 fn render_raw_input(
926 &self,
927 window: &mut Window,
928 cx: &mut Context<Self>,
929 ) -> Option<gpui::Stateful<Div>> {
930 let theme_settings = ThemeSettings::get_global(cx);
931 let buffer_font_size = theme_settings.buffer_font_size(cx);
932
933 Some(
934 v_flex()
935 .size_full()
936 .overflow_hidden()
937 .relative()
938 .child(
939 div()
940 .id("raw-input")
941 .py_4()
942 .px_6()
943 .size_full()
944 .bg(cx.theme().colors().editor_background)
945 .overflow_scroll()
946 .child(if let Some(active_prediction) = &self.active_prediction {
947 markdown::MarkdownElement::new(
948 active_prediction.formatted_inputs.clone(),
949 MarkdownStyle {
950 base_text_style: window.text_style(),
951 syntax: cx.theme().syntax().clone(),
952 code_block: StyleRefinement {
953 text: TextStyleRefinement {
954 font_family: Some(
955 theme_settings.buffer_font.family.clone(),
956 ),
957 font_size: Some(buffer_font_size.into()),
958 ..Default::default()
959 },
960 padding: EdgesRefinement {
961 top: Some(DefiniteLength::Absolute(
962 AbsoluteLength::Pixels(px(8.)),
963 )),
964 left: Some(DefiniteLength::Absolute(
965 AbsoluteLength::Pixels(px(8.)),
966 )),
967 right: Some(DefiniteLength::Absolute(
968 AbsoluteLength::Pixels(px(8.)),
969 )),
970 bottom: Some(DefiniteLength::Absolute(
971 AbsoluteLength::Pixels(px(8.)),
972 )),
973 },
974 margin: EdgesRefinement {
975 top: Some(Length::Definite(px(8.).into())),
976 left: Some(Length::Definite(px(0.).into())),
977 right: Some(Length::Definite(px(0.).into())),
978 bottom: Some(Length::Definite(px(12.).into())),
979 },
980 border_style: Some(BorderStyle::Solid),
981 border_widths: EdgesRefinement {
982 top: Some(AbsoluteLength::Pixels(px(1.))),
983 left: Some(AbsoluteLength::Pixels(px(1.))),
984 right: Some(AbsoluteLength::Pixels(px(1.))),
985 bottom: Some(AbsoluteLength::Pixels(px(1.))),
986 },
987 border_color: Some(cx.theme().colors().border_variant),
988 background: Some(
989 cx.theme().colors().editor_background.into(),
990 ),
991 ..Default::default()
992 },
993 ..Default::default()
994 },
995 )
996 .into_any_element()
997 } else {
998 div()
999 .child("No active completion".to_string())
1000 .into_any_element()
1001 }),
1002 )
1003 .id("raw-input-view"),
1004 )
1005 }
1006
1007 fn render_active_completion(
1008 &mut self,
1009 window: &mut Window,
1010 cx: &mut Context<Self>,
1011 ) -> Option<impl IntoElement> {
1012 let active_prediction = self.active_prediction.as_ref()?;
1013 let completion_id = active_prediction.prediction.id.clone();
1014 let focus_handle = &self.focus_handle(cx);
1015
1016 let border_color = cx.theme().colors().border;
1017 let bg_color = cx.theme().colors().editor_background;
1018
1019 let rated = self.ep_store.read(cx).is_prediction_rated(&completion_id);
1020 let feedback_empty = active_prediction
1021 .feedback_editor
1022 .read(cx)
1023 .text(cx)
1024 .is_empty();
1025
1026 let label_container = h_flex().pl_1().gap_1p5();
1027
1028 Some(
1029 v_flex()
1030 .size_full()
1031 .overflow_hidden()
1032 .relative()
1033 .child(
1034 v_flex()
1035 .size_full()
1036 .overflow_hidden()
1037 .relative()
1038 .child(self.render_view_nav(cx))
1039 .when_some(
1040 match self.current_view {
1041 RatePredictionView::SuggestedEdits => {
1042 self.render_suggested_edits(cx)
1043 }
1044 RatePredictionView::RawInput => self.render_raw_input(window, cx),
1045 },
1046 |this, element| this.child(element),
1047 ),
1048 )
1049 .when(!rated, |this| {
1050 let modal = cx.entity().downgrade();
1051 let failure_mode_menu =
1052 ContextMenu::build(window, cx, move |menu, _window, _cx| {
1053 FeedbackCompletionProvider::FAILURE_MODES
1054 .iter()
1055 .fold(menu, |menu, (key, description)| {
1056 let key: SharedString = (*key).into();
1057 let description: SharedString = (*description).into();
1058 let modal = modal.clone();
1059 menu.entry(
1060 format!("{} {}", key, description),
1061 None,
1062 move |window, cx| {
1063 if let Some(modal) = modal.upgrade() {
1064 modal.update(cx, |this, cx| {
1065 if let Some(active) = &this.active_prediction {
1066 active.feedback_editor.update(
1067 cx,
1068 |editor, cx| {
1069 editor.set_text(
1070 format!("{} {}", key, description),
1071 window,
1072 cx,
1073 );
1074 },
1075 );
1076 }
1077 });
1078 }
1079 },
1080 )
1081 })
1082 });
1083
1084 this.child(
1085 h_flex()
1086 .p_2()
1087 .gap_2()
1088 .border_y_1()
1089 .border_color(border_color)
1090 .child(
1091 DropdownMenu::new(
1092 "failure-mode-dropdown",
1093 "Issue",
1094 failure_mode_menu,
1095 )
1096 .handle(self.failure_mode_menu_handle.clone())
1097 .style(ui::DropdownStyle::Outlined)
1098 .trigger_size(ButtonSize::Compact),
1099 )
1100 .child(
1101 h_flex()
1102 .gap_2()
1103 .child(
1104 Icon::new(IconName::Info)
1105 .size(IconSize::XSmall)
1106 .color(Color::Muted),
1107 )
1108 .child(
1109 div().flex_wrap().child(
1110 Label::new(concat!(
1111 "Explain why this completion is good or bad. ",
1112 "If it's negative, describe what you expected instead."
1113 ))
1114 .size(LabelSize::Small)
1115 .color(Color::Muted),
1116 ),
1117 ),
1118 ),
1119 )
1120 })
1121 .when(!rated, |this| {
1122 this.child(
1123 div()
1124 .h_40()
1125 .pt_1()
1126 .bg(bg_color)
1127 .child(active_prediction.feedback_editor.clone()),
1128 )
1129 })
1130 .child(
1131 h_flex()
1132 .p_1()
1133 .h_8()
1134 .max_h_8()
1135 .border_t_1()
1136 .border_color(border_color)
1137 .max_w_full()
1138 .justify_between()
1139 .children(if rated {
1140 Some(
1141 label_container
1142 .child(
1143 Icon::new(IconName::Check)
1144 .size(IconSize::Small)
1145 .color(Color::Success),
1146 )
1147 .child(Label::new("Rated completion.").color(Color::Muted)),
1148 )
1149 } else if active_prediction.prediction.edits.is_empty() {
1150 Some(
1151 label_container
1152 .child(
1153 Icon::new(IconName::Warning)
1154 .size(IconSize::Small)
1155 .color(Color::Warning),
1156 )
1157 .child(Label::new("No edits produced.").color(Color::Muted)),
1158 )
1159 } else {
1160 Some(label_container)
1161 })
1162 .child(
1163 h_flex()
1164 .gap_1()
1165 .child(
1166 Button::new("bad", "Bad Prediction")
1167 .start_icon(Icon::new(IconName::ThumbsDown).size(IconSize::Small))
1168 .disabled(rated || feedback_empty)
1169 .when(feedback_empty, |this| {
1170 this.tooltip(Tooltip::text(
1171 "Explain what's bad about it before reporting it",
1172 ))
1173 })
1174 .key_binding(KeyBinding::for_action_in(
1175 &ThumbsDownActivePrediction,
1176 focus_handle,
1177 cx,
1178 ))
1179 .on_click(cx.listener(move |this, _, window, cx| {
1180 if this.active_prediction.is_some() {
1181 this.thumbs_down_active(
1182 &ThumbsDownActivePrediction,
1183 window,
1184 cx,
1185 );
1186 }
1187 })),
1188 )
1189 .child(
1190 Button::new("good", "Good Prediction")
1191 .start_icon(Icon::new(IconName::ThumbsUp).size(IconSize::Small))
1192 .disabled(rated)
1193 .key_binding(KeyBinding::for_action_in(
1194 &ThumbsUpActivePrediction,
1195 focus_handle,
1196 cx,
1197 ))
1198 .on_click(cx.listener(move |this, _, window, cx| {
1199 if this.active_prediction.is_some() {
1200 this.thumbs_up_active(
1201 &ThumbsUpActivePrediction,
1202 window,
1203 cx,
1204 );
1205 }
1206 })),
1207 ),
1208 ),
1209 ),
1210 )
1211 }
1212
1213 fn render_shown_completions(&self, cx: &Context<Self>) -> impl Iterator<Item = ListItem> {
1214 self.ep_store
1215 .read(cx)
1216 .rateable_predictions()
1217 .cloned()
1218 .enumerate()
1219 .map(|(index, completion)| {
1220 let selected = self
1221 .active_prediction
1222 .as_ref()
1223 .is_some_and(|selected| selected.prediction.id == completion.id);
1224 let rated = self.ep_store.read(cx).is_prediction_rated(&completion.id);
1225
1226 let (icon_name, icon_color, tooltip_text) =
1227 match (rated, completion.edits.is_empty()) {
1228 (true, _) => (IconName::Check, Color::Success, "Rated Prediction"),
1229 (false, true) => (IconName::File, Color::Muted, "No Edits Produced"),
1230 (false, false) => (IconName::FileDiff, Color::Accent, "Edits Available"),
1231 };
1232 let (trigger_icon, trigger_tooltip) = match completion.trigger {
1233 PredictEditsRequestTrigger::Testing => (IconName::Debug, "Testing"),
1234 PredictEditsRequestTrigger::Diagnostics => {
1235 (IconName::ToolDiagnostics, "Diagnostics")
1236 }
1237 PredictEditsRequestTrigger::DiagnosticNavigation => {
1238 (IconName::ArrowRight, "Diagnostic Navigation")
1239 }
1240 PredictEditsRequestTrigger::Cli => (IconName::Terminal, "CLI"),
1241 PredictEditsRequestTrigger::Explicit => (IconName::Person, "Explicit"),
1242 PredictEditsRequestTrigger::BufferEdit => (IconName::Pencil, "Buffer Edit"),
1243 PredictEditsRequestTrigger::LSPCompletionAccepted => {
1244 (IconName::Code, "LSP Completion Accepted")
1245 }
1246 PredictEditsRequestTrigger::PredictionAccepted => {
1247 (IconName::OmegaPredict, "Prediction Accepted")
1248 }
1249 PredictEditsRequestTrigger::PredictionPartiallyAccepted => {
1250 (IconName::CheckDouble, "Prediction Partially Accepted")
1251 }
1252 PredictEditsRequestTrigger::EditorCreated => (IconName::File, "Editor Created"),
1253 PredictEditsRequestTrigger::ProviderChanged => {
1254 (IconName::Settings, "Provider Changed")
1255 }
1256 PredictEditsRequestTrigger::UserInfoChanged => {
1257 (IconName::Person, "User Info Changed")
1258 }
1259 PredictEditsRequestTrigger::VimModeChanged => {
1260 (IconName::Keyboard, "Vim Mode Changed")
1261 }
1262 PredictEditsRequestTrigger::SettingsChanged => {
1263 (IconName::Settings, "Settings Changed")
1264 }
1265 PredictEditsRequestTrigger::Other => (IconName::CircleHelp, "Other"),
1266 };
1267
1268 let file = completion.buffer.read(cx).file();
1269 let file_name = file.as_ref().map_or(
1270 SharedString::new_static(MultiBuffer::DEFAULT_TITLE),
1271 |file| file.file_name(cx).to_string().into(),
1272 );
1273 let file_path = file.map(|file| file.path().as_unix_str().to_string());
1274
1275 ListItem::new(completion.id.clone())
1276 .inset(true)
1277 .spacing(ListItemSpacing::Sparse)
1278 .focused(index == self.selected_index)
1279 .toggle_state(selected)
1280 .child(
1281 h_flex()
1282 .id("completion-content")
1283 .gap_3()
1284 .child(Icon::new(icon_name).color(icon_color).size(IconSize::Small))
1285 .child(
1286 Icon::new(trigger_icon)
1287 .color(Color::Muted)
1288 .size(IconSize::XSmall),
1289 )
1290 .child(
1291 v_flex().child(
1292 h_flex()
1293 .gap_1()
1294 .child(Label::new(file_name).size(LabelSize::Small))
1295 .when_some(file_path, |this, p| {
1296 this.child(
1297 Label::new(p)
1298 .size(LabelSize::Small)
1299 .color(Color::Muted),
1300 )
1301 }),
1302 ),
1303 ),
1304 )
1305 .tooltip(Tooltip::text(format!(
1306 "{tooltip_text} • Trigger: {trigger_tooltip}"
1307 )))
1308 .on_click(cx.listener(move |this, _, window, cx| {
1309 this.select_completion(Some(completion.clone()), true, window, cx);
1310 }))
1311 })
1312 }
1313}
1314
1315impl Render for RatePredictionsModal {
1316 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1317 let border_color = cx.theme().colors().border;
1318
1319 h_flex()
1320 .key_context("RatePredictionModal")
1321 .track_focus(&self.focus_handle)
1322 .on_action(cx.listener(Self::dismiss))
1323 .on_action(cx.listener(Self::confirm))
1324 .on_action(cx.listener(Self::select_previous))
1325 .on_action(cx.listener(Self::select_prev_edit))
1326 .on_action(cx.listener(Self::select_next))
1327 .on_action(cx.listener(Self::select_next_edit))
1328 .on_action(cx.listener(Self::select_first))
1329 .on_action(cx.listener(Self::select_last))
1330 .on_action(cx.listener(Self::thumbs_up_active))
1331 .on_action(cx.listener(Self::thumbs_down_active))
1332 .on_action(cx.listener(Self::focus_completions))
1333 .on_action(cx.listener(Self::preview_completion))
1334 .bg(cx.theme().colors().elevated_surface_background)
1335 .border_1()
1336 .border_color(border_color)
1337 .w(window.viewport_size().width - px(320.))
1338 .h(window.viewport_size().height - px(300.))
1339 .rounded_lg()
1340 .shadow_lg()
1341 .child(
1342 v_flex()
1343 .w_72()
1344 .h_full()
1345 .border_r_1()
1346 .border_color(border_color)
1347 .flex_shrink_0()
1348 .overflow_hidden()
1349 .child({
1350 let icons = self.ep_store.read(cx).icons(cx);
1351 h_flex()
1352 .h_8()
1353 .px_2()
1354 .justify_between()
1355 .border_b_1()
1356 .border_color(border_color)
1357 .child(Icon::new(icons.base).size(IconSize::Small))
1358 .child(
1359 Label::new("From most recent to oldest")
1360 .color(Color::Muted)
1361 .size(LabelSize::Small),
1362 )
1363 })
1364 .child(
1365 div()
1366 .id("completion_list")
1367 .p_0p5()
1368 .h_full()
1369 .overflow_y_scroll()
1370 .child(
1371 List::new()
1372 .empty_message(
1373 div()
1374 .p_2()
1375 .child(
1376 Label::new(concat!(
1377 "No completions yet. ",
1378 "Use the editor to generate some, ",
1379 "and make sure to rate them!"
1380 ))
1381 .color(Color::Muted),
1382 )
1383 .into_any_element(),
1384 )
1385 .children(self.render_shown_completions(cx)),
1386 ),
1387 ),
1388 )
1389 .children(self.render_active_completion(window, cx))
1390 .on_mouse_down_out(cx.listener(|this, _, _, cx| {
1391 if !this.failure_mode_menu_handle.is_deployed() {
1392 cx.emit(DismissEvent);
1393 }
1394 }))
1395 }
1396}
1397
1398impl EventEmitter<DismissEvent> for RatePredictionsModal {}
1399
1400impl Focusable for RatePredictionsModal {
1401 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1402 self.focus_handle.clone()
1403 }
1404}
1405
1406impl ModalView for RatePredictionsModal {}
1407
1408struct FeedbackCompletionProvider;
1409
1410impl FeedbackCompletionProvider {
1411 const FAILURE_MODES: &'static [(&'static str, &'static str)] = &[
1412 ("@location", "Unexpected location"),
1413 ("@malformed", "Incomplete, cut off, or syntax error"),
1414 (
1415 "@deleted",
1416 "Deleted code that should be kept (use `@reverted` if it undid a recent edit)",
1417 ),
1418 ("@style", "Wrong coding style or conventions"),
1419 ("@repetitive", "Repeated existing code"),
1420 ("@hallucinated", "Referenced non-existent symbols"),
1421 ("@formatting", "Wrong indentation or structure"),
1422 ("@aggressive", "Changed more than expected"),
1423 ("@conservative", "Too cautious, changed too little"),
1424 ("@context", "Ignored or misunderstood context"),
1425 ("@reverted", "Undid recent edits"),
1426 ("@cursor_position", "Cursor placed in unhelpful position"),
1427 ("@whitespace", "Unwanted whitespace or newline changes"),
1428 ];
1429}
1430
1431impl editor::CompletionProvider for FeedbackCompletionProvider {
1432 fn completions(
1433 &self,
1434 buffer: &Entity<Buffer>,
1435 buffer_position: language::Anchor,
1436 _trigger: editor::CompletionContext,
1437 _window: &mut Window,
1438 cx: &mut Context<Editor>,
1439 ) -> gpui::Task<anyhow::Result<Vec<CompletionResponse>>> {
1440 let buffer = buffer.read(cx);
1441 let mut count_back = 0;
1442
1443 for char in buffer.reversed_chars_at(buffer_position) {
1444 if char.is_ascii_alphanumeric() || char == '_' || char == '@' {
1445 count_back += 1;
1446 } else {
1447 break;
1448 }
1449 }
1450
1451 let start_anchor = buffer.anchor_before(
1452 buffer_position
1453 .to_offset(&buffer)
1454 .saturating_sub(count_back),
1455 );
1456
1457 let replace_range = start_anchor..buffer_position;
1458 let snapshot = buffer.text_snapshot();
1459 let query: String = snapshot.text_for_range(replace_range.clone()).collect();
1460
1461 if !query.starts_with('@') {
1462 return gpui::Task::ready(Ok(vec![CompletionResponse {
1463 completions: vec![],
1464 display_options: CompletionDisplayOptions {
1465 dynamic_width: true,
1466 },
1467 is_incomplete: false,
1468 }]));
1469 }
1470
1471 let query_lower = query.to_lowercase();
1472
1473 let completions: Vec<Completion> = Self::FAILURE_MODES
1474 .iter()
1475 .filter(|(key, _description)| key.starts_with(&query_lower))
1476 .map(|(key, description)| Completion {
1477 replace_range: replace_range.clone(),
1478 new_text: format!("{} {}", key, description),
1479 label: CodeLabel::plain(format!("{}: {}", key, description), None),
1480 documentation: None,
1481 source: CompletionSource::Custom,
1482 icon_path: None,
1483 icon_color: None,
1484 match_start: None,
1485 snippet_deduplication_key: None,
1486 insert_text_mode: None,
1487 confirm: None,
1488 group: None,
1489 })
1490 .collect();
1491
1492 gpui::Task::ready(Ok(vec![CompletionResponse {
1493 completions,
1494 display_options: CompletionDisplayOptions {
1495 dynamic_width: true,
1496 },
1497 is_incomplete: false,
1498 }]))
1499 }
1500
1501 fn is_completion_trigger(
1502 &self,
1503 _buffer: &Entity<Buffer>,
1504 _position: language::Anchor,
1505 text: &str,
1506 _trigger_in_words: bool,
1507 _cx: &mut Context<Editor>,
1508 ) -> bool {
1509 text.chars()
1510 .last()
1511 .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_' || c == '@')
1512 }
1513}
1514