Skip to repository content2599 lines · 97.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:32:29.745Z 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
edit_prediction.rs
1use super::*;
2
3pub fn make_suggestion_styles(cx: &App) -> EditPredictionStyles {
4 EditPredictionStyles {
5 insertion: HighlightStyle {
6 color: Some(cx.theme().status().predictive),
7 ..HighlightStyle::default()
8 },
9 whitespace: HighlightStyle {
10 background_color: Some(cx.theme().status().created_background),
11 ..HighlightStyle::default()
12 },
13 }
14}
15
16pub(super) enum EditDisplayMode {
17 TabAccept,
18 DiffPopover,
19 Inline,
20}
21
22pub(super) enum EditPrediction {
23 Edit {
24 // TODO could be a language::Anchor?
25 edits: Vec<(Range<Anchor>, Arc<str>)>,
26 /// Predicted cursor position as (anchor, offset_from_anchor).
27 /// The anchor is in multibuffer coordinates; after applying edits,
28 /// resolve the anchor and add the offset to get the final cursor position.
29 cursor_position: Option<(Anchor, usize)>,
30 edit_preview: Option<EditPreview>,
31 display_mode: EditDisplayMode,
32 snapshot: BufferSnapshot,
33 },
34 /// Move to a specific location in the active editor
35 MoveWithin {
36 target: Anchor,
37 snapshot: BufferSnapshot,
38 },
39 /// Move to a specific location in a different editor (not the active one)
40 MoveOutside {
41 target: language::Anchor,
42 snapshot: BufferSnapshot,
43 },
44}
45
46pub(super) struct EditPredictionState {
47 pub(super) inlay_ids: Vec<InlayId>,
48 pub(super) completion: EditPrediction,
49 pub(super) completion_id: Option<SharedString>,
50 pub(super) invalidation_range: Option<Range<Anchor>>,
51}
52
53pub(super) enum EditPredictionSettings {
54 Disabled,
55 Enabled {
56 show_in_menu: bool,
57 preview_requires_modifier: bool,
58 },
59}
60
61pub(super) enum MenuEditPredictionsPolicy {
62 #[cfg(test)]
63 Never,
64 ByProvider,
65}
66
67pub(super) enum EditPredictionPreview {
68 /// Modifier is not pressed
69 Inactive { released_too_fast: bool },
70 /// Modifier pressed
71 Active {
72 since: Instant,
73 previous_scroll_position: Option<SharedScrollAnchor>,
74 },
75}
76
77#[derive(Copy, Clone, Eq, PartialEq)]
78pub(super) enum EditPredictionKeybindSurface {
79 Inline,
80 CursorPopoverCompact,
81 CursorPopoverExpanded,
82}
83
84#[derive(Copy, Clone, Eq, PartialEq, Debug)]
85pub(super) enum EditPredictionKeybindAction {
86 Accept,
87 Preview,
88}
89
90pub(super) struct EditPredictionKeybindDisplay {
91 #[cfg(test)]
92 pub(super) accept_keystroke: Option<gpui::KeybindingKeystroke>,
93 #[cfg(test)]
94 pub(super) preview_keystroke: Option<gpui::KeybindingKeystroke>,
95 pub(super) displayed_keystroke: Option<gpui::KeybindingKeystroke>,
96 pub(super) action: EditPredictionKeybindAction,
97 pub(super) missing_accept_keystroke: bool,
98 pub(super) show_hold_label: bool,
99}
100
101impl EditPredictionPreview {
102 pub(super) fn released_too_fast(&self) -> bool {
103 match self {
104 EditPredictionPreview::Inactive { released_too_fast } => *released_too_fast,
105 EditPredictionPreview::Active { .. } => false,
106 }
107 }
108
109 pub(super) fn set_previous_scroll_position(
110 &mut self,
111 scroll_position: Option<SharedScrollAnchor>,
112 ) {
113 if let EditPredictionPreview::Active {
114 previous_scroll_position,
115 ..
116 } = self
117 {
118 *previous_scroll_position = scroll_position;
119 }
120 }
121}
122
123pub(super) struct RegisteredEditPredictionDelegate {
124 pub(super) provider: Arc<dyn EditPredictionDelegateHandle>,
125 _subscription: Subscription,
126}
127
128pub(super) fn edit_prediction_edit_text(
129 current_snapshot: &BufferSnapshot,
130 edits: &[(Range<Anchor>, impl AsRef<str>)],
131 edit_preview: &EditPreview,
132 include_deletions: bool,
133 multibuffer_snapshot: &MultiBufferSnapshot,
134 cx: &App,
135) -> HighlightedText {
136 let edits = edits
137 .iter()
138 .filter_map(|(anchor, text)| {
139 Some((
140 multibuffer_snapshot
141 .anchor_range_to_buffer_anchor_range(anchor.clone())?
142 .1,
143 text,
144 ))
145 })
146 .collect::<Vec<_>>();
147
148 edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
149}
150
151impl Editor {
152 pub fn set_edit_prediction_provider<T>(
153 &mut self,
154 provider: Option<Entity<T>>,
155 trigger: EditPredictionRequestTrigger,
156 window: &mut Window,
157 cx: &mut Context<Self>,
158 ) where
159 T: EditPredictionDelegate,
160 {
161 self.edit_prediction_provider = provider.map(|provider| RegisteredEditPredictionDelegate {
162 _subscription: cx.observe_in(&provider, window, |this, _, window, cx| {
163 if this.focus_handle.is_focused(window) {
164 this.update_visible_edit_prediction(window, cx);
165 }
166 }),
167 provider: Arc::new(provider),
168 });
169 self.update_edit_prediction_settings(cx);
170 self.refresh_edit_prediction(false, false, trigger, window, cx);
171 }
172
173 pub fn set_edit_predictions_hidden_for_vim_mode(
174 &mut self,
175 hidden: bool,
176 window: &mut Window,
177 cx: &mut Context<Self>,
178 ) {
179 if hidden != self.edit_predictions_hidden_for_vim_mode {
180 self.edit_predictions_hidden_for_vim_mode = hidden;
181 if hidden {
182 self.update_visible_edit_prediction(window, cx);
183 } else {
184 self.refresh_edit_prediction(
185 true,
186 false,
187 EditPredictionRequestTrigger::VimModeChanged,
188 window,
189 cx,
190 );
191 }
192 }
193 }
194
195 pub fn toggle_edit_predictions(
196 &mut self,
197 _: &ToggleEditPrediction,
198 window: &mut Window,
199 cx: &mut Context<Self>,
200 ) {
201 if self.show_edit_predictions_override.is_some() {
202 self.set_show_edit_predictions(None, window, cx);
203 } else {
204 let show_edit_predictions = !self.edit_predictions_enabled();
205 self.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
206 }
207 }
208
209 pub fn set_show_edit_predictions(
210 &mut self,
211 show_edit_predictions: Option<bool>,
212 window: &mut Window,
213 cx: &mut Context<Self>,
214 ) {
215 self.show_edit_predictions_override = show_edit_predictions;
216 self.update_edit_prediction_settings(cx);
217
218 if let Some(false) = show_edit_predictions {
219 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
220 } else {
221 self.refresh_edit_prediction(
222 false,
223 true,
224 EditPredictionRequestTrigger::Explicit,
225 window,
226 cx,
227 );
228 }
229 }
230
231 pub fn refresh_edit_prediction(
232 &mut self,
233 debounce: bool,
234 user_requested: bool,
235 trigger: EditPredictionRequestTrigger,
236 window: &mut Window,
237 cx: &mut Context<Self>,
238 ) -> Option<()> {
239 if self.leader_id.is_some() {
240 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
241 return None;
242 }
243
244 let cursor = self.selections.newest_anchor().head();
245 let (buffer, cursor_buffer_position) =
246 self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
247
248 if DisableAiSettings::is_ai_disabled_for_buffer(Some(&buffer), cx) {
249 return None;
250 }
251
252 if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) {
253 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
254 return None;
255 }
256
257 self.update_visible_edit_prediction(window, cx);
258
259 if !user_requested
260 && (!self.should_show_edit_predictions()
261 || !self.is_focused(window)
262 || buffer.read(cx).is_empty())
263 {
264 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
265 return None;
266 }
267
268 self.edit_prediction_provider()?.refresh(
269 buffer,
270 cursor_buffer_position,
271 debounce,
272 trigger,
273 cx,
274 );
275 Some(())
276 }
277
278 pub fn edit_predictions_enabled(&self) -> bool {
279 match self.edit_prediction_settings {
280 EditPredictionSettings::Disabled => false,
281 EditPredictionSettings::Enabled { .. } => true,
282 }
283 }
284
285 pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
286 if self.edit_prediction_provider.is_none() {
287 self.edit_prediction_settings = EditPredictionSettings::Disabled;
288 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
289 return;
290 }
291
292 let selection = self.selections.newest_anchor();
293 let cursor = selection.head();
294
295 if let Some((buffer, cursor_buffer_position)) =
296 self.buffer.read(cx).text_anchor_for_position(cursor, cx)
297 {
298 if DisableAiSettings::is_ai_disabled_for_buffer(Some(&buffer), cx) {
299 self.edit_prediction_settings = EditPredictionSettings::Disabled;
300 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
301 return;
302 }
303 self.edit_prediction_settings =
304 self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
305 }
306 }
307
308 pub fn edit_prediction_preview_is_active(&self) -> bool {
309 matches!(
310 self.edit_prediction_preview,
311 EditPredictionPreview::Active { .. }
312 )
313 }
314
315 pub fn edit_predictions_enabled_at_cursor(&self, cx: &App) -> bool {
316 let cursor = self.selections.newest_anchor().head();
317 if let Some((buffer, cursor_position)) =
318 self.buffer.read(cx).text_anchor_for_position(cursor, cx)
319 {
320 self.edit_predictions_enabled_in_buffer(&buffer, cursor_position, cx)
321 } else {
322 false
323 }
324 }
325
326 pub fn show_edit_prediction(
327 &mut self,
328 _: &ShowEditPrediction,
329 window: &mut Window,
330 cx: &mut Context<Self>,
331 ) {
332 if !self.has_active_edit_prediction() {
333 self.refresh_edit_prediction(
334 false,
335 true,
336 EditPredictionRequestTrigger::Explicit,
337 window,
338 cx,
339 );
340 return;
341 }
342
343 self.update_visible_edit_prediction(window, cx);
344 }
345
346 pub fn accept_partial_edit_prediction(
347 &mut self,
348 granularity: EditPredictionGranularity,
349 window: &mut Window,
350 cx: &mut Context<Self>,
351 ) {
352 if self.read_only(cx) {
353 return;
354 }
355 if self.show_edit_predictions_in_menu() {
356 self.hide_context_menu(window, cx);
357 }
358
359 let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else {
360 return;
361 };
362
363 if !matches!(granularity, EditPredictionGranularity::Full) && self.selections.count() != 1 {
364 return;
365 }
366
367 match &active_edit_prediction.completion {
368 EditPrediction::MoveWithin { target, .. } => {
369 let target = *target;
370
371 if matches!(granularity, EditPredictionGranularity::Full) {
372 if let Some(position_map) = &self.last_position_map {
373 let target_row = target.to_display_point(&position_map.snapshot).row();
374 let is_visible = position_map.visible_row_range.contains(&target_row);
375
376 if is_visible || !self.edit_prediction_requires_modifier() {
377 self.unfold_ranges(&[target..target], true, false, cx);
378 self.change_selections(
379 SelectionEffects::scroll(Autoscroll::newest()),
380 window,
381 cx,
382 |selections| {
383 selections.select_anchor_ranges([target..target]);
384 },
385 );
386 self.clear_row_highlights::<EditPredictionPreview>();
387 self.edit_prediction_preview
388 .set_previous_scroll_position(None);
389 } else {
390 // Highlight and request scroll
391 self.edit_prediction_preview
392 .set_previous_scroll_position(Some(
393 position_map.snapshot.scroll_anchor,
394 ));
395 self.highlight_rows::<EditPredictionPreview>(
396 target..target,
397 |cx| cx.theme().colors().editor_highlighted_line_background,
398 RowHighlightOptions {
399 autoscroll: true,
400 ..Default::default()
401 },
402 cx,
403 );
404 self.request_autoscroll(Autoscroll::fit(), cx);
405 }
406 }
407 } else {
408 self.change_selections(
409 SelectionEffects::scroll(Autoscroll::newest()),
410 window,
411 cx,
412 |selections| {
413 selections.select_anchor_ranges([target..target]);
414 },
415 );
416 }
417 }
418 EditPrediction::MoveOutside { snapshot, target } => {
419 if let Some(workspace) = self.workspace() {
420 Self::open_editor_at_anchor(snapshot, *target, &workspace, window, cx)
421 .detach_and_log_err(cx);
422 }
423 }
424 EditPrediction::Edit {
425 edits,
426 cursor_position,
427 ..
428 } => {
429 self.report_edit_prediction_event(
430 active_edit_prediction.completion_id.clone(),
431 true,
432 cx,
433 );
434
435 match granularity {
436 EditPredictionGranularity::Full => {
437 let transaction_id_prev = self.buffer.read(cx).last_transaction_id(cx);
438
439 // Compute fallback cursor position BEFORE applying the edit,
440 // so the anchor tracks through the edit correctly
441 let fallback_cursor_target = {
442 let snapshot = self.buffer.read(cx).snapshot(cx);
443 let Some((last_edit_range, _)) = edits.last() else {
444 return;
445 };
446 last_edit_range.end.bias_right(&snapshot)
447 };
448
449 self.buffer.update(cx, |buffer, cx| {
450 buffer.edit(edits.iter().cloned(), None, cx)
451 });
452
453 if let Some(provider) = self.edit_prediction_provider() {
454 provider.accept(cx);
455 }
456
457 // Resolve cursor position after the edit is applied
458 let cursor_target = if let Some((anchor, offset)) = cursor_position {
459 // The anchor tracks through the edit, then we add the offset
460 let snapshot = self.buffer.read(cx).snapshot(cx);
461 let base_offset = anchor.to_offset(&snapshot).0;
462 let target_offset =
463 MultiBufferOffset((base_offset + offset).min(snapshot.len().0));
464 snapshot.anchor_after(target_offset)
465 } else {
466 fallback_cursor_target
467 };
468
469 self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
470 s.select_anchor_ranges([cursor_target..cursor_target]);
471 });
472
473 let selections = self.selections.disjoint_anchors_arc();
474 if let Some(transaction_id_now) =
475 self.buffer.read(cx).last_transaction_id(cx)
476 {
477 if transaction_id_prev != Some(transaction_id_now) {
478 self.selection_history
479 .insert_transaction(transaction_id_now, selections);
480 }
481 }
482
483 self.update_visible_edit_prediction(window, cx);
484 if self.active_edit_prediction.is_none() {
485 self.refresh_edit_prediction(
486 true,
487 true,
488 EditPredictionRequestTrigger::PredictionAccepted,
489 window,
490 cx,
491 );
492 }
493 cx.notify();
494 }
495 _ => {
496 let snapshot = self.buffer.read(cx).snapshot(cx);
497 let cursor_offset = self
498 .selections
499 .newest::<MultiBufferOffset>(&self.display_snapshot(cx))
500 .head();
501
502 let insertion = edits.iter().find_map(|(range, text)| {
503 let range = range.to_offset(&snapshot);
504 if range.is_empty() && range.start == cursor_offset {
505 Some(text)
506 } else {
507 None
508 }
509 });
510
511 if let Some(text) = insertion {
512 let text_to_insert = match granularity {
513 EditPredictionGranularity::Word => {
514 let mut partial = text
515 .chars()
516 .by_ref()
517 .take_while(|c| c.is_alphabetic())
518 .collect::<String>();
519 if partial.is_empty() {
520 partial = text
521 .chars()
522 .by_ref()
523 .take_while(|c| c.is_whitespace() || !c.is_alphabetic())
524 .collect::<String>();
525 }
526 partial
527 }
528 EditPredictionGranularity::Line => {
529 if let Some(line) = text.split_inclusive('\n').next() {
530 line.to_string()
531 } else {
532 text.to_string()
533 }
534 }
535 EditPredictionGranularity::Full => unreachable!(),
536 };
537
538 cx.emit(EditorEvent::InputHandled {
539 utf16_range_to_replace: None,
540 text: text_to_insert.clone().into(),
541 });
542
543 self.replace_selections(&text_to_insert, None, window, cx, false);
544 self.refresh_edit_prediction(
545 true,
546 true,
547 EditPredictionRequestTrigger::PredictionPartiallyAccepted,
548 window,
549 cx,
550 );
551 cx.notify();
552 } else {
553 self.accept_partial_edit_prediction(
554 EditPredictionGranularity::Full,
555 window,
556 cx,
557 );
558 }
559 }
560 }
561 }
562 }
563 }
564
565 pub fn accept_next_word_edit_prediction(
566 &mut self,
567 _: &AcceptNextWordEditPrediction,
568 window: &mut Window,
569 cx: &mut Context<Self>,
570 ) {
571 self.accept_partial_edit_prediction(EditPredictionGranularity::Word, window, cx);
572 }
573
574 pub fn accept_next_line_edit_prediction(
575 &mut self,
576 _: &AcceptNextLineEditPrediction,
577 window: &mut Window,
578 cx: &mut Context<Self>,
579 ) {
580 self.accept_partial_edit_prediction(EditPredictionGranularity::Line, window, cx);
581 }
582
583 pub fn accept_edit_prediction(
584 &mut self,
585 _: &AcceptEditPrediction,
586 window: &mut Window,
587 cx: &mut Context<Self>,
588 ) {
589 self.accept_partial_edit_prediction(EditPredictionGranularity::Full, window, cx);
590 }
591
592 pub fn has_active_edit_prediction(&self) -> bool {
593 self.active_edit_prediction.is_some()
594 }
595
596 /// Returns true when we're displaying the edit prediction popover below the cursor
597 /// like we are not previewing and the LSP autocomplete menu is visible
598 /// or we are in `when_holding_modifier` mode.
599 pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool {
600 if self.edit_prediction_preview_is_active()
601 || !self.show_edit_predictions_in_menu()
602 || !self.edit_predictions_enabled()
603 {
604 return false;
605 }
606
607 if self.has_visible_completions_menu() {
608 return true;
609 }
610
611 has_completion && self.edit_prediction_requires_modifier()
612 }
613
614 pub fn edit_prediction_provider(&self) -> Option<Arc<dyn EditPredictionDelegateHandle>> {
615 Some(self.edit_prediction_provider.as_ref()?.provider.clone())
616 }
617
618 pub(super) fn preview_edit_prediction_keystroke(
619 &self,
620 window: &mut Window,
621 cx: &mut App,
622 ) -> Option<gpui::KeybindingKeystroke> {
623 let key_context = self.key_context_internal(true, window, cx);
624 let bindings = window.bindings_for_action_in_context(&AcceptEditPrediction, key_context);
625 bindings
626 .into_iter()
627 .rev()
628 .find_map(|binding| match binding.keystrokes() {
629 [keystroke, ..] if keystroke.modifiers().modified() => Some(keystroke.clone()),
630 _ => None,
631 })
632 }
633
634 pub(super) fn edit_prediction_keybind_display(
635 &self,
636 surface: EditPredictionKeybindSurface,
637 window: &mut Window,
638 cx: &mut App,
639 ) -> EditPredictionKeybindDisplay {
640 let accept_keystroke =
641 self.accept_edit_prediction_keystroke(EditPredictionGranularity::Full, window, cx);
642 let preview_keystroke = self.preview_edit_prediction_keystroke(window, cx);
643
644 let action = match surface {
645 EditPredictionKeybindSurface::Inline
646 | EditPredictionKeybindSurface::CursorPopoverCompact => {
647 if self.edit_prediction_requires_modifier() {
648 EditPredictionKeybindAction::Preview
649 } else {
650 EditPredictionKeybindAction::Accept
651 }
652 }
653 EditPredictionKeybindSurface::CursorPopoverExpanded => self
654 .active_edit_prediction
655 .as_ref()
656 .filter(|completion| {
657 self.edit_prediction_cursor_popover_prefers_preview(completion, cx)
658 })
659 .map_or(EditPredictionKeybindAction::Accept, |_| {
660 EditPredictionKeybindAction::Preview
661 }),
662 };
663 #[cfg(test)]
664 let preview_copy = preview_keystroke.clone();
665 #[cfg(test)]
666 let accept_copy = accept_keystroke.clone();
667
668 let displayed_keystroke = match surface {
669 EditPredictionKeybindSurface::Inline => match action {
670 EditPredictionKeybindAction::Accept => accept_keystroke,
671 EditPredictionKeybindAction::Preview => preview_keystroke,
672 },
673 EditPredictionKeybindSurface::CursorPopoverCompact
674 | EditPredictionKeybindSurface::CursorPopoverExpanded => match action {
675 EditPredictionKeybindAction::Accept => accept_keystroke,
676 EditPredictionKeybindAction::Preview => {
677 preview_keystroke.or_else(|| accept_keystroke.clone())
678 }
679 },
680 };
681
682 let missing_accept_keystroke = displayed_keystroke.is_none();
683
684 EditPredictionKeybindDisplay {
685 #[cfg(test)]
686 accept_keystroke: accept_copy,
687 #[cfg(test)]
688 preview_keystroke: preview_copy,
689 displayed_keystroke,
690 action,
691 missing_accept_keystroke,
692 show_hold_label: matches!(surface, EditPredictionKeybindSurface::CursorPopoverCompact)
693 && self.edit_prediction_preview.released_too_fast(),
694 }
695 }
696
697 pub(super) fn show_edit_predictions_in_menu(&self) -> bool {
698 match self.edit_prediction_settings {
699 EditPredictionSettings::Disabled => false,
700 EditPredictionSettings::Enabled { show_in_menu, .. } => show_in_menu,
701 }
702 }
703
704 pub(super) fn edit_prediction_requires_modifier(&self) -> bool {
705 match self.edit_prediction_settings {
706 EditPredictionSettings::Disabled => false,
707 EditPredictionSettings::Enabled {
708 preview_requires_modifier,
709 ..
710 } => preview_requires_modifier,
711 }
712 }
713
714 pub(super) fn discard_edit_prediction(
715 &mut self,
716 reason: EditPredictionDiscardReason,
717 cx: &mut Context<Self>,
718 ) -> bool {
719 if reason == EditPredictionDiscardReason::Rejected {
720 let completion_id = self
721 .active_edit_prediction
722 .as_ref()
723 .and_then(|active_completion| active_completion.completion_id.clone());
724
725 self.report_edit_prediction_event(completion_id, false, cx);
726 }
727
728 if let Some(provider) = self.edit_prediction_provider() {
729 provider.discard(reason, cx);
730 }
731
732 self.take_active_edit_prediction(reason == EditPredictionDiscardReason::Ignored, cx)
733 }
734
735 pub(super) fn take_active_edit_prediction(
736 &mut self,
737 preserve_stale_in_menu: bool,
738 cx: &mut Context<Self>,
739 ) -> bool {
740 let Some(active_edit_prediction) = self.active_edit_prediction.take() else {
741 if !preserve_stale_in_menu {
742 self.stale_edit_prediction_in_menu = None;
743 }
744 return false;
745 };
746
747 self.splice_inlays(&active_edit_prediction.inlay_ids, Default::default(), cx);
748 self.clear_highlights(HighlightKey::EditPredictionHighlight, cx);
749 self.stale_edit_prediction_in_menu =
750 preserve_stale_in_menu.then_some(active_edit_prediction);
751 true
752 }
753
754 pub(super) fn update_edit_prediction_preview(
755 &mut self,
756 modifiers: &Modifiers,
757 window: &mut Window,
758 cx: &mut Context<Self>,
759 ) {
760 let modifiers_held = self.edit_prediction_preview_modifiers_held(modifiers, window, cx);
761
762 if modifiers_held {
763 if matches!(
764 self.edit_prediction_preview,
765 EditPredictionPreview::Inactive { .. }
766 ) {
767 self.edit_prediction_preview = EditPredictionPreview::Active {
768 previous_scroll_position: None,
769 since: Instant::now(),
770 };
771
772 self.update_visible_edit_prediction(window, cx);
773 cx.notify();
774 }
775 } else if let EditPredictionPreview::Active {
776 previous_scroll_position,
777 since,
778 } = self.edit_prediction_preview
779 {
780 if let (Some(previous_scroll_position), Some(position_map)) =
781 (previous_scroll_position, self.last_position_map.as_ref())
782 {
783 self.set_scroll_position(
784 previous_scroll_position
785 .scroll_position(&position_map.snapshot.display_snapshot),
786 window,
787 cx,
788 );
789 }
790
791 self.edit_prediction_preview = EditPredictionPreview::Inactive {
792 released_too_fast: since.elapsed() < Duration::from_millis(200),
793 };
794 self.clear_row_highlights::<EditPredictionPreview>();
795 self.update_visible_edit_prediction(window, cx);
796 cx.notify();
797 }
798 }
799
800 pub(super) fn update_visible_edit_prediction(
801 &mut self,
802 _window: &mut Window,
803 cx: &mut Context<Self>,
804 ) -> Option<()> {
805 if self.ime_transaction.is_some() {
806 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
807 return None;
808 }
809
810 let selection = self.selections.newest_anchor();
811 let multibuffer = self.buffer.read(cx).snapshot(cx);
812 let cursor = selection.head();
813 let (cursor_text_anchor, _) = multibuffer.anchor_to_buffer_anchor(cursor)?;
814 let buffer = self.buffer.read(cx).buffer(cursor_text_anchor.buffer_id)?;
815
816 // Check project-level disable_ai setting for the current buffer
817 if DisableAiSettings::is_ai_disabled_for_buffer(Some(&buffer), cx) {
818 return None;
819 }
820 let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
821
822 let show_in_menu = self.show_edit_predictions_in_menu();
823 let completions_menu_has_precedence = !show_in_menu
824 && (self.context_menu.borrow().is_some()
825 || (!self.completion_tasks.is_empty() && !self.has_active_edit_prediction()));
826
827 if completions_menu_has_precedence
828 || !offset_selection.is_empty()
829 || self
830 .active_edit_prediction
831 .as_ref()
832 .is_some_and(|completion| {
833 let Some(invalidation_range) = completion.invalidation_range.as_ref() else {
834 return false;
835 };
836 let invalidation_range = invalidation_range.to_offset(&multibuffer);
837 let invalidation_range = invalidation_range.start..=invalidation_range.end;
838 !invalidation_range.contains(&offset_selection.head())
839 })
840 {
841 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
842 return None;
843 }
844
845 self.take_active_edit_prediction(true, cx);
846 let Some(provider) = self.edit_prediction_provider() else {
847 self.edit_prediction_settings = EditPredictionSettings::Disabled;
848 return None;
849 };
850
851 self.edit_prediction_settings =
852 self.edit_prediction_settings_at_position(&buffer, cursor_text_anchor, cx);
853
854 self.in_leading_whitespace = multibuffer.is_line_whitespace_upto(cursor);
855
856 if self.in_leading_whitespace {
857 let cursor_point = cursor.to_point(&multibuffer);
858 let mut suggested_indent = None;
859 multibuffer.suggested_indents_callback(
860 cursor_point.row..cursor_point.row + 1,
861 &mut |_, indent| {
862 suggested_indent = Some(indent);
863 ControlFlow::Break(())
864 },
865 cx,
866 );
867
868 if let Some(indent) = suggested_indent
869 && indent.len == cursor_point.column
870 {
871 self.in_leading_whitespace = false;
872 }
873 }
874
875 let edit_prediction = provider.suggest(&buffer, cursor_text_anchor, cx)?;
876
877 let (completion_id, edits, predicted_cursor_position, edit_preview) = match edit_prediction
878 {
879 edit_prediction_types::EditPrediction::Local {
880 id,
881 edits,
882 cursor_position,
883 edit_preview,
884 } => (id, edits, cursor_position, edit_preview),
885 edit_prediction_types::EditPrediction::Jump {
886 id,
887 snapshot,
888 target,
889 } => {
890 if let Some(provider) = &self.edit_prediction_provider {
891 provider.provider.did_show(SuggestionDisplayType::Jump, cx);
892 }
893 self.stale_edit_prediction_in_menu = None;
894 self.active_edit_prediction = Some(EditPredictionState {
895 inlay_ids: vec![],
896 completion: EditPrediction::MoveOutside { snapshot, target },
897 completion_id: id,
898 invalidation_range: None,
899 });
900 cx.notify();
901 return Some(());
902 }
903 };
904
905 let edits = edits
906 .into_iter()
907 .flat_map(|(range, new_text)| {
908 Some((
909 multibuffer.buffer_anchor_range_to_anchor_range(range)?,
910 new_text,
911 ))
912 })
913 .collect::<Vec<_>>();
914 if edits.is_empty() {
915 return None;
916 }
917
918 let cursor_position = predicted_cursor_position.and_then(|predicted| {
919 let anchor = multibuffer.anchor_in_excerpt(predicted.anchor)?;
920 Some((anchor, predicted.offset))
921 });
922
923 let Some((first_edit_range, _)) = edits.first() else {
924 return None;
925 };
926 let Some((last_edit_range, _)) = edits.last() else {
927 return None;
928 };
929
930 let first_edit_start = first_edit_range.start;
931 let first_edit_start_point = first_edit_start.to_point(&multibuffer);
932 let edit_start_row = first_edit_start_point.row.saturating_sub(2);
933
934 let last_edit_end = last_edit_range.end;
935 let last_edit_end_point = last_edit_end.to_point(&multibuffer);
936 let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2);
937
938 let cursor_row = cursor.to_point(&multibuffer).row;
939
940 let snapshot = multibuffer
941 .buffer_for_id(cursor_text_anchor.buffer_id)
942 .cloned()?;
943
944 let mut inlay_ids = Vec::new();
945 let invalidation_row_range;
946 let move_invalidation_row_range = if cursor_row < edit_start_row {
947 Some(cursor_row..edit_end_row)
948 } else if cursor_row > edit_end_row {
949 Some(edit_start_row..cursor_row)
950 } else {
951 None
952 };
953 let supports_jump = self
954 .edit_prediction_provider
955 .as_ref()
956 .map(|provider| provider.provider.supports_jump_to_edit())
957 .unwrap_or(true);
958
959 let is_move = supports_jump
960 && (move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode);
961 let completion = if is_move {
962 if let Some(provider) = &self.edit_prediction_provider {
963 provider.provider.did_show(SuggestionDisplayType::Jump, cx);
964 }
965 invalidation_row_range =
966 move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row);
967
968 let (_, snapshot) = multibuffer.anchor_to_buffer_anchor(first_edit_start)?;
969
970 EditPrediction::MoveWithin {
971 target: first_edit_start,
972 snapshot: snapshot.clone(),
973 }
974 } else {
975 let show_completions_in_menu = self.has_visible_completions_menu();
976 let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true)
977 && !self.edit_predictions_hidden_for_vim_mode;
978
979 let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) {
980 if provider.show_tab_accept_marker() {
981 EditDisplayMode::TabAccept
982 } else {
983 EditDisplayMode::Inline
984 }
985 } else {
986 EditDisplayMode::DiffPopover
987 };
988
989 let report_shown = match display_mode {
990 EditDisplayMode::DiffPopover | EditDisplayMode::Inline => {
991 show_completions_in_buffer || show_completions_in_menu
992 }
993 EditDisplayMode::TabAccept => {
994 show_completions_in_menu || self.edit_prediction_preview_is_active()
995 }
996 };
997
998 if report_shown && let Some(provider) = &self.edit_prediction_provider {
999 let suggestion_display_type = match display_mode {
1000 EditDisplayMode::DiffPopover => SuggestionDisplayType::DiffPopover,
1001 EditDisplayMode::Inline | EditDisplayMode::TabAccept => {
1002 SuggestionDisplayType::GhostText
1003 }
1004 };
1005 provider.provider.did_show(suggestion_display_type, cx);
1006 }
1007
1008 if show_completions_in_buffer {
1009 if edits
1010 .iter()
1011 .all(|(range, _)| range.to_offset(&multibuffer).is_empty())
1012 {
1013 let mut inlays = Vec::new();
1014 for (range, new_text) in &edits {
1015 let inlay = Inlay::edit_prediction(
1016 post_inc(&mut self.next_inlay_id),
1017 range.start,
1018 new_text.as_ref(),
1019 );
1020 inlay_ids.push(inlay.id);
1021 inlays.push(inlay);
1022 }
1023
1024 self.splice_inlays(&[], inlays, cx);
1025 } else {
1026 let background_color = cx.theme().status().deleted_background;
1027 self.highlight_text(
1028 HighlightKey::EditPredictionHighlight,
1029 edits.iter().map(|(range, _)| range.clone()).collect(),
1030 HighlightStyle {
1031 background_color: Some(background_color),
1032 ..Default::default()
1033 },
1034 cx,
1035 );
1036 }
1037 }
1038
1039 invalidation_row_range = edit_start_row..edit_end_row;
1040
1041 EditPrediction::Edit {
1042 edits,
1043 cursor_position,
1044 edit_preview,
1045 display_mode,
1046 snapshot,
1047 }
1048 };
1049
1050 let invalidation_range = multibuffer
1051 .anchor_before(Point::new(invalidation_row_range.start, 0))
1052 ..multibuffer.anchor_after(Point::new(
1053 invalidation_row_range.end,
1054 multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)),
1055 ));
1056
1057 self.stale_edit_prediction_in_menu = None;
1058 self.active_edit_prediction = Some(EditPredictionState {
1059 inlay_ids,
1060 completion,
1061 completion_id,
1062 invalidation_range: Some(invalidation_range),
1063 });
1064
1065 cx.notify();
1066
1067 Some(())
1068 }
1069
1070 pub(super) fn render_edit_prediction_popover(
1071 &mut self,
1072 text_bounds: &Bounds<Pixels>,
1073 content_origin: gpui::Point<Pixels>,
1074 right_margin: Pixels,
1075 editor_snapshot: &EditorSnapshot,
1076 visible_row_range: Range<DisplayRow>,
1077 scroll_top: ScrollOffset,
1078 scroll_bottom: ScrollOffset,
1079 line_layouts: &[LineWithInvisibles],
1080 line_height: Pixels,
1081 scroll_position: gpui::Point<ScrollOffset>,
1082 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
1083 newest_selection_head: Option<DisplayPoint>,
1084 editor_width: Pixels,
1085 style: &EditorStyle,
1086 window: &mut Window,
1087 cx: &mut App,
1088 ) -> Option<(AnyElement, gpui::Point<Pixels>)> {
1089 if self.mode().is_minimap() {
1090 return None;
1091 }
1092 let active_edit_prediction = self.active_edit_prediction.as_ref()?;
1093
1094 if self.edit_prediction_visible_in_cursor_popover(true) {
1095 return None;
1096 }
1097
1098 match &active_edit_prediction.completion {
1099 EditPrediction::MoveWithin { target, .. } => {
1100 let target_display_point = target.to_display_point(editor_snapshot);
1101
1102 if self.edit_prediction_requires_modifier() {
1103 if !self.edit_prediction_preview_is_active() {
1104 return None;
1105 }
1106
1107 self.render_edit_prediction_modifier_jump_popover(
1108 text_bounds,
1109 content_origin,
1110 visible_row_range,
1111 line_layouts,
1112 line_height,
1113 scroll_pixel_position,
1114 newest_selection_head,
1115 target_display_point,
1116 window,
1117 cx,
1118 )
1119 } else {
1120 self.render_edit_prediction_eager_jump_popover(
1121 text_bounds,
1122 content_origin,
1123 editor_snapshot,
1124 visible_row_range,
1125 scroll_top,
1126 scroll_bottom,
1127 line_height,
1128 scroll_pixel_position,
1129 target_display_point,
1130 editor_width,
1131 window,
1132 cx,
1133 )
1134 }
1135 }
1136 EditPrediction::Edit {
1137 display_mode: EditDisplayMode::Inline,
1138 ..
1139 } => None,
1140 EditPrediction::Edit {
1141 display_mode: EditDisplayMode::TabAccept,
1142 edits,
1143 ..
1144 } => {
1145 let range = &edits.first()?.0;
1146 let target_display_point = range.end.to_display_point(editor_snapshot);
1147
1148 self.render_edit_prediction_end_of_line_popover(
1149 "Accept",
1150 editor_snapshot,
1151 visible_row_range,
1152 target_display_point,
1153 line_height,
1154 scroll_pixel_position,
1155 content_origin,
1156 editor_width,
1157 window,
1158 cx,
1159 )
1160 }
1161 EditPrediction::Edit {
1162 edits,
1163 edit_preview,
1164 display_mode: EditDisplayMode::DiffPopover,
1165 snapshot,
1166 ..
1167 } => self.render_edit_prediction_diff_popover(
1168 text_bounds,
1169 content_origin,
1170 right_margin,
1171 editor_snapshot,
1172 visible_row_range,
1173 line_layouts,
1174 line_height,
1175 scroll_position,
1176 scroll_pixel_position,
1177 newest_selection_head,
1178 editor_width,
1179 style,
1180 edits,
1181 edit_preview,
1182 snapshot,
1183 window,
1184 cx,
1185 ),
1186 EditPrediction::MoveOutside { snapshot, .. } => {
1187 let mut element = self
1188 .render_edit_prediction_jump_outside_popover(snapshot, window, cx)
1189 .into_any();
1190
1191 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
1192 let origin_x = text_bounds.size.width - size.width - px(30.);
1193 let origin = text_bounds.origin + gpui::Point::new(origin_x, px(16.));
1194 element.prepaint_at(origin, window, cx);
1195
1196 Some((element, origin))
1197 }
1198 }
1199 }
1200
1201 pub(super) fn edit_prediction_cursor_popover_height(&self) -> Pixels {
1202 px(30.)
1203 }
1204
1205 pub(super) fn render_edit_prediction_cursor_popover(
1206 &self,
1207 min_width: Pixels,
1208 max_width: Pixels,
1209 cursor_point: Point,
1210 style: &EditorStyle,
1211 window: &mut Window,
1212 cx: &mut Context<Editor>,
1213 ) -> Option<AnyElement> {
1214 let provider = self.edit_prediction_provider.as_ref()?;
1215 let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
1216
1217 let is_refreshing = provider.provider.is_refreshing(cx);
1218
1219 fn pending_completion_container(icon: IconName) -> Div {
1220 h_flex().h_full().flex_1().gap_2().child(Icon::new(icon))
1221 }
1222
1223 let completion = match &self.active_edit_prediction {
1224 Some(prediction) => {
1225 if !self.has_visible_completions_menu() {
1226 const RADIUS: Pixels = px(6.);
1227 const BORDER_WIDTH: Pixels = px(1.);
1228 let keybind_display = self.edit_prediction_keybind_display(
1229 EditPredictionKeybindSurface::CursorPopoverCompact,
1230 window,
1231 cx,
1232 );
1233
1234 return Some(
1235 h_flex()
1236 .elevation_2(cx)
1237 .border(BORDER_WIDTH)
1238 .border_color(cx.theme().colors().border)
1239 .when(keybind_display.missing_accept_keystroke, |el| {
1240 el.border_color(cx.theme().status().error)
1241 })
1242 .rounded(RADIUS)
1243 .rounded_tl(px(0.))
1244 .overflow_hidden()
1245 .child(div().px_1p5().child(match &prediction.completion {
1246 EditPrediction::MoveWithin { target, snapshot } => {
1247 use text::ToPoint as _;
1248 if target.text_anchor_in(&snapshot).to_point(snapshot).row
1249 > cursor_point.row
1250 {
1251 Icon::new(icons.down)
1252 } else {
1253 Icon::new(icons.up)
1254 }
1255 }
1256 EditPrediction::MoveOutside { .. } => {
1257 // TODO [zeta2] custom icon for external jump?
1258 Icon::new(icons.base)
1259 }
1260 EditPrediction::Edit { .. } => Icon::new(icons.base),
1261 }))
1262 .child(
1263 h_flex()
1264 .gap_1()
1265 .py_1()
1266 .px_2()
1267 .rounded_r(RADIUS - BORDER_WIDTH)
1268 .border_l_1()
1269 .border_color(cx.theme().colors().border)
1270 .bg(Self::edit_prediction_line_popover_bg_color(cx))
1271 .when(keybind_display.show_hold_label, |el| {
1272 el.child(
1273 Label::new("Hold")
1274 .size(LabelSize::Small)
1275 .when(
1276 keybind_display.missing_accept_keystroke,
1277 |el| el.strikethrough(),
1278 )
1279 .line_height_style(LineHeightStyle::UiLabel),
1280 )
1281 })
1282 .id("edit_prediction_cursor_popover_keybind")
1283 .when(keybind_display.missing_accept_keystroke, |el| {
1284 let status_colors = cx.theme().status();
1285
1286 el.bg(status_colors.error_background)
1287 .border_color(status_colors.error.opacity(0.6))
1288 .child(Icon::new(IconName::Info).color(Color::Error))
1289 .cursor_default()
1290 .hoverable_tooltip(move |_window, cx| {
1291 cx.new(|_| MissingEditPredictionKeybindingTooltip)
1292 .into()
1293 })
1294 })
1295 .when_some(
1296 keybind_display.displayed_keystroke.as_ref(),
1297 |el, compact_keystroke| {
1298 el.child(self.render_edit_prediction_popover_keystroke(
1299 compact_keystroke,
1300 Color::Default,
1301 cx,
1302 ))
1303 },
1304 ),
1305 )
1306 .into_any(),
1307 );
1308 }
1309
1310 self.render_edit_prediction_cursor_popover_preview(
1311 prediction,
1312 cursor_point,
1313 style,
1314 cx,
1315 )?
1316 }
1317
1318 None if is_refreshing => match &self.stale_edit_prediction_in_menu {
1319 Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview(
1320 stale_completion,
1321 cursor_point,
1322 style,
1323 cx,
1324 )?,
1325
1326 None => pending_completion_container(icons.base)
1327 .child(Label::new("...").size(LabelSize::Small)),
1328 },
1329
1330 None => pending_completion_container(icons.base)
1331 .child(Label::new("...").size(LabelSize::Small)),
1332 };
1333
1334 let completion = if is_refreshing || self.active_edit_prediction.is_none() {
1335 completion
1336 .with_animation(
1337 "loading-completion",
1338 Animation::new(Duration::from_secs(2))
1339 .repeat()
1340 .with_easing(pulsating_between(0.4, 0.8)),
1341 |label, delta| label.opacity(delta),
1342 )
1343 .into_any_element()
1344 } else {
1345 completion.into_any_element()
1346 };
1347
1348 let has_completion = self.active_edit_prediction.is_some();
1349 let keybind_display = self.edit_prediction_keybind_display(
1350 EditPredictionKeybindSurface::CursorPopoverExpanded,
1351 window,
1352 cx,
1353 );
1354
1355 Some(
1356 h_flex()
1357 .min_w(min_width)
1358 .max_w(max_width)
1359 .flex_1()
1360 .elevation_2(cx)
1361 .border_color(cx.theme().colors().border)
1362 .child(
1363 div()
1364 .flex_1()
1365 .py_1()
1366 .px_2()
1367 .overflow_hidden()
1368 .child(completion),
1369 )
1370 .when_some(
1371 keybind_display.displayed_keystroke.as_ref(),
1372 |el, keystroke| {
1373 let key_color = if !has_completion {
1374 Color::Muted
1375 } else {
1376 Color::Default
1377 };
1378
1379 if keybind_display.action == EditPredictionKeybindAction::Preview {
1380 el.child(
1381 h_flex()
1382 .h_full()
1383 .border_l_1()
1384 .rounded_r_lg()
1385 .border_color(cx.theme().colors().border)
1386 .bg(Self::edit_prediction_line_popover_bg_color(cx))
1387 .gap_1()
1388 .py_1()
1389 .px_2()
1390 .child(self.render_edit_prediction_popover_keystroke(
1391 keystroke, key_color, cx,
1392 ))
1393 .child(Label::new("Preview").into_any_element())
1394 .opacity(if has_completion { 1.0 } else { 0.4 }),
1395 )
1396 } else {
1397 el.child(
1398 h_flex()
1399 .h_full()
1400 .border_l_1()
1401 .rounded_r_lg()
1402 .border_color(cx.theme().colors().border)
1403 .bg(Self::edit_prediction_line_popover_bg_color(cx))
1404 .gap_1()
1405 .py_1()
1406 .px_2()
1407 .child(self.render_edit_prediction_popover_keystroke(
1408 keystroke, key_color, cx,
1409 ))
1410 .opacity(if has_completion { 1.0 } else { 0.4 }),
1411 )
1412 }
1413 },
1414 )
1415 .into_any(),
1416 )
1417 }
1418
1419 fn accept_edit_prediction_keystroke(
1420 &self,
1421 granularity: EditPredictionGranularity,
1422 window: &mut Window,
1423 cx: &mut App,
1424 ) -> Option<gpui::KeybindingKeystroke> {
1425 let key_context = self.key_context_internal(true, window, cx);
1426
1427 let bindings =
1428 match granularity {
1429 EditPredictionGranularity::Word => window
1430 .bindings_for_action_in_context(&AcceptNextWordEditPrediction, key_context),
1431 EditPredictionGranularity::Line => window
1432 .bindings_for_action_in_context(&AcceptNextLineEditPrediction, key_context),
1433 EditPredictionGranularity::Full => {
1434 window.bindings_for_action_in_context(&AcceptEditPrediction, key_context)
1435 }
1436 };
1437
1438 bindings
1439 .into_iter()
1440 .rev()
1441 .find_map(|binding| match binding.keystrokes() {
1442 [keystroke, ..] => Some(keystroke.clone()),
1443 _ => None,
1444 })
1445 }
1446
1447 fn edit_prediction_preview_modifiers_held(
1448 &self,
1449 modifiers: &Modifiers,
1450 window: &mut Window,
1451 cx: &mut App,
1452 ) -> bool {
1453 let can_supersede_active_menu =
1454 self.context_menu.borrow().as_ref().is_none_or(|menu| {
1455 !menu.visible() || matches!(menu, CodeContextMenu::Completions(_))
1456 });
1457
1458 if !can_supersede_active_menu {
1459 return false;
1460 }
1461
1462 let key_context = self.key_context_internal(true, window, cx);
1463 let actions: [&dyn Action; 3] = [
1464 &AcceptEditPrediction,
1465 &AcceptNextWordEditPrediction,
1466 &AcceptNextLineEditPrediction,
1467 ];
1468
1469 actions.into_iter().any(|action| {
1470 window
1471 .bindings_for_action_in_context(action, key_context.clone())
1472 .into_iter()
1473 .rev()
1474 .any(|binding| {
1475 binding.keystrokes().first().is_some_and(|keystroke| {
1476 keystroke.modifiers().modified() && keystroke.modifiers() == modifiers
1477 })
1478 })
1479 })
1480 }
1481
1482 fn edit_prediction_cursor_popover_prefers_preview(
1483 &self,
1484 completion: &EditPredictionState,
1485 cx: &App,
1486 ) -> bool {
1487 let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx);
1488
1489 match &completion.completion {
1490 EditPrediction::Edit {
1491 edits, snapshot, ..
1492 } => {
1493 let mut start_row: Option<u32> = None;
1494 let mut end_row: Option<u32> = None;
1495
1496 for (range, text) in edits {
1497 let Some((_, range)) =
1498 multibuffer_snapshot.anchor_range_to_buffer_anchor_range(range.clone())
1499 else {
1500 continue;
1501 };
1502 let edit_start_row = range.start.to_point(snapshot).row;
1503 let old_end_row = range.end.to_point(snapshot).row;
1504 let inserted_newline_count = text
1505 .as_ref()
1506 .chars()
1507 .filter(|character| *character == '\n')
1508 .count() as u32;
1509 let deleted_newline_count = old_end_row - edit_start_row;
1510 let preview_end_row = edit_start_row + inserted_newline_count;
1511
1512 start_row =
1513 Some(start_row.map_or(edit_start_row, |row| row.min(edit_start_row)));
1514 end_row = Some(end_row.map_or(preview_end_row, |row| row.max(preview_end_row)));
1515
1516 if deleted_newline_count > 1 {
1517 end_row = Some(end_row.map_or(old_end_row, |row| row.max(old_end_row)));
1518 }
1519 }
1520
1521 start_row
1522 .zip(end_row)
1523 .is_some_and(|(start_row, end_row)| end_row > start_row)
1524 }
1525 EditPrediction::MoveWithin { .. } | EditPrediction::MoveOutside { .. } => false,
1526 }
1527 }
1528
1529 fn edit_predictions_disabled_in_scope(
1530 &self,
1531 buffer: &Entity<Buffer>,
1532 buffer_position: language::Anchor,
1533 cx: &App,
1534 ) -> bool {
1535 let snapshot = buffer.read(cx).snapshot();
1536 let settings = snapshot.settings_at(buffer_position, cx);
1537
1538 let Some(scope) = snapshot.language_scope_at(buffer_position) else {
1539 return false;
1540 };
1541
1542 scope.override_name().is_some_and(|scope_name| {
1543 settings
1544 .edit_predictions_disabled_in
1545 .iter()
1546 .any(|s| s == scope_name)
1547 })
1548 }
1549
1550 fn edit_prediction_settings_at_position(
1551 &self,
1552 buffer: &Entity<Buffer>,
1553 buffer_position: language::Anchor,
1554 cx: &App,
1555 ) -> EditPredictionSettings {
1556 if !self.mode.is_full()
1557 || !self.show_edit_predictions_override.unwrap_or(true)
1558 || self.edit_predictions_disabled_in_scope(buffer, buffer_position, cx)
1559 {
1560 return EditPredictionSettings::Disabled;
1561 }
1562
1563 if !LanguageSettings::for_buffer(&buffer.read(cx), cx).show_edit_predictions {
1564 return EditPredictionSettings::Disabled;
1565 };
1566
1567 let by_provider = matches!(
1568 self.menu_edit_predictions_policy,
1569 MenuEditPredictionsPolicy::ByProvider
1570 );
1571
1572 let show_in_menu = by_provider
1573 && self
1574 .edit_prediction_provider
1575 .as_ref()
1576 .is_some_and(|provider| provider.provider.show_predictions_in_menu());
1577
1578 let file = buffer.read(cx).file();
1579 let preview_requires_modifier =
1580 all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle;
1581
1582 EditPredictionSettings::Enabled {
1583 show_in_menu,
1584 preview_requires_modifier,
1585 }
1586 }
1587
1588 fn should_show_edit_predictions(&self) -> bool {
1589 self.snippet_stack.is_empty() && self.edit_predictions_enabled()
1590 }
1591
1592 fn edit_predictions_enabled_in_buffer(
1593 &self,
1594 buffer: &Entity<Buffer>,
1595 buffer_position: language::Anchor,
1596 cx: &App,
1597 ) -> bool {
1598 maybe!({
1599 if self.read_only(cx) || self.leader_id.is_some() {
1600 return Some(false);
1601 }
1602 let provider = self.edit_prediction_provider()?;
1603 if !provider.is_enabled(buffer, buffer_position, cx) {
1604 return Some(false);
1605 }
1606 let buffer = buffer.read(cx);
1607 let Some(file) = buffer.file() else {
1608 return Some(true);
1609 };
1610 let settings = all_language_settings(Some(file), cx);
1611 Some(settings.edit_predictions_enabled_for_file(file, cx))
1612 })
1613 .unwrap_or(false)
1614 }
1615
1616 fn report_edit_prediction_event(&self, id: Option<SharedString>, accepted: bool, cx: &App) {
1617 let Some(provider) = self.edit_prediction_provider() else {
1618 return;
1619 };
1620
1621 let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
1622 let Some((position, _)) =
1623 buffer_snapshot.anchor_to_buffer_anchor(self.selections.newest_anchor().head())
1624 else {
1625 return;
1626 };
1627 let Some(buffer) = self.buffer.read(cx).buffer(position.buffer_id) else {
1628 return;
1629 };
1630
1631 let extension = buffer
1632 .read(cx)
1633 .file()
1634 .and_then(|file| Some(file.path().extension()?.to_string()));
1635
1636 let event_type = match accepted {
1637 true => "Edit Prediction Accepted",
1638 false => "Edit Prediction Discarded",
1639 };
1640 telemetry::event!(
1641 event_type,
1642 provider = provider.name(),
1643 prediction_id = id,
1644 suggestion_accepted = accepted,
1645 file_extension = extension,
1646 );
1647 }
1648
1649 fn open_editor_at_anchor(
1650 snapshot: &language::BufferSnapshot,
1651 target: language::Anchor,
1652 workspace: &Entity<Workspace>,
1653 window: &mut Window,
1654 cx: &mut App,
1655 ) -> Task<Result<()>> {
1656 workspace.update(cx, |workspace, cx| {
1657 let path = snapshot.file().map(|file| file.full_path(cx));
1658 let Some(project_path) = path
1659 .as_ref()
1660 .and_then(|path| workspace.project().read(cx).find_project_path(path, cx))
1661 else {
1662 return Task::ready(Err(anyhow::anyhow!(
1663 "project path not found for edit prediction target {path:?}"
1664 )));
1665 };
1666 let target = text::ToPoint::to_point(&target, snapshot);
1667 let item = workspace.open_path(project_path, None, true, window, cx);
1668 window.spawn(cx, async move |cx| {
1669 let Some(editor) = item.await?.downcast::<Editor>() else {
1670 return Ok(());
1671 };
1672 editor
1673 .update_in(cx, |editor, window, cx| {
1674 editor.go_to_singleton_buffer_point(target, window, cx);
1675 })
1676 .ok();
1677 anyhow::Ok(())
1678 })
1679 })
1680 }
1681
1682 const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = px(24.);
1683
1684 const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = px(2.);
1685
1686 fn render_edit_prediction_modifier_jump_popover(
1687 &mut self,
1688 text_bounds: &Bounds<Pixels>,
1689 content_origin: gpui::Point<Pixels>,
1690 visible_row_range: Range<DisplayRow>,
1691 line_layouts: &[LineWithInvisibles],
1692 line_height: Pixels,
1693 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
1694 newest_selection_head: Option<DisplayPoint>,
1695 target_display_point: DisplayPoint,
1696 window: &mut Window,
1697 cx: &mut App,
1698 ) -> Option<(AnyElement, gpui::Point<Pixels>)> {
1699 let scrolled_content_origin =
1700 content_origin - gpui::Point::new(scroll_pixel_position.x.into(), Pixels::ZERO);
1701
1702 const SCROLL_PADDING_Y: Pixels = px(12.);
1703
1704 if target_display_point.row() < visible_row_range.start {
1705 return self.render_edit_prediction_scroll_popover(
1706 &|_| SCROLL_PADDING_Y,
1707 IconName::ArrowUp,
1708 visible_row_range,
1709 line_layouts,
1710 newest_selection_head,
1711 scrolled_content_origin,
1712 window,
1713 cx,
1714 );
1715 } else if target_display_point.row() >= visible_row_range.end {
1716 return self.render_edit_prediction_scroll_popover(
1717 &|size| text_bounds.size.height - size.height - SCROLL_PADDING_Y,
1718 IconName::ArrowDown,
1719 visible_row_range,
1720 line_layouts,
1721 newest_selection_head,
1722 scrolled_content_origin,
1723 window,
1724 cx,
1725 );
1726 }
1727
1728 const POLE_WIDTH: Pixels = px(2.);
1729
1730 let line_layout =
1731 line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?;
1732 let target_column = target_display_point.column() as usize;
1733
1734 let target_x = line_layout.x_for_index(target_column);
1735 let target_y = (target_display_point.row().as_f64() * f64::from(line_height))
1736 - scroll_pixel_position.y;
1737
1738 let flag_on_right = target_x < text_bounds.size.width / 2.;
1739
1740 let mut border_color = Self::edit_prediction_callout_popover_border_color(cx);
1741 border_color.l += 0.001;
1742
1743 let mut element = v_flex()
1744 .items_end()
1745 .when(flag_on_right, |el| el.items_start())
1746 .child(if flag_on_right {
1747 self.render_edit_prediction_line_popover("Jump", None, window, cx)
1748 .rounded_bl(px(0.))
1749 .rounded_tl(px(0.))
1750 .border_l_2()
1751 .border_color(border_color)
1752 } else {
1753 self.render_edit_prediction_line_popover("Jump", None, window, cx)
1754 .rounded_br(px(0.))
1755 .rounded_tr(px(0.))
1756 .border_r_2()
1757 .border_color(border_color)
1758 })
1759 .child(div().w(POLE_WIDTH).bg(border_color).h(line_height))
1760 .into_any();
1761
1762 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
1763
1764 let mut origin = scrolled_content_origin + point(target_x, target_y.into())
1765 - point(
1766 if flag_on_right {
1767 POLE_WIDTH
1768 } else {
1769 size.width - POLE_WIDTH
1770 },
1771 size.height - line_height,
1772 );
1773
1774 origin.x = origin.x.max(content_origin.x);
1775
1776 element.prepaint_at(origin, window, cx);
1777
1778 Some((element, origin))
1779 }
1780
1781 fn render_edit_prediction_scroll_popover(
1782 &mut self,
1783 to_y: &dyn Fn(Size<Pixels>) -> Pixels,
1784 scroll_icon: IconName,
1785 visible_row_range: Range<DisplayRow>,
1786 line_layouts: &[LineWithInvisibles],
1787 newest_selection_head: Option<DisplayPoint>,
1788 scrolled_content_origin: gpui::Point<Pixels>,
1789 window: &mut Window,
1790 cx: &mut App,
1791 ) -> Option<(AnyElement, gpui::Point<Pixels>)> {
1792 let mut element = self
1793 .render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)
1794 .into_any();
1795
1796 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
1797
1798 let cursor = newest_selection_head?;
1799 let cursor_row_layout =
1800 line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?;
1801 let cursor_column = cursor.column() as usize;
1802
1803 let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
1804
1805 let origin = scrolled_content_origin + point(cursor_character_x, to_y(size));
1806
1807 element.prepaint_at(origin, window, cx);
1808 Some((element, origin))
1809 }
1810
1811 fn render_edit_prediction_eager_jump_popover(
1812 &mut self,
1813 text_bounds: &Bounds<Pixels>,
1814 content_origin: gpui::Point<Pixels>,
1815 editor_snapshot: &EditorSnapshot,
1816 visible_row_range: Range<DisplayRow>,
1817 scroll_top: ScrollOffset,
1818 scroll_bottom: ScrollOffset,
1819 line_height: Pixels,
1820 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
1821 target_display_point: DisplayPoint,
1822 editor_width: Pixels,
1823 window: &mut Window,
1824 cx: &mut App,
1825 ) -> Option<(AnyElement, gpui::Point<Pixels>)> {
1826 if target_display_point.row().as_f64() < scroll_top {
1827 let mut element = self
1828 .render_edit_prediction_line_popover(
1829 "Jump to Edit",
1830 Some(IconName::ArrowUp),
1831 window,
1832 cx,
1833 )
1834 .into_any();
1835
1836 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
1837 let offset = point(
1838 (text_bounds.size.width - size.width) / 2.,
1839 Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
1840 );
1841
1842 let origin = text_bounds.origin + offset;
1843 element.prepaint_at(origin, window, cx);
1844 Some((element, origin))
1845 } else if (target_display_point.row().as_f64() + 1.) > scroll_bottom {
1846 let mut element = self
1847 .render_edit_prediction_line_popover(
1848 "Jump to Edit",
1849 Some(IconName::ArrowDown),
1850 window,
1851 cx,
1852 )
1853 .into_any();
1854
1855 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
1856 let offset = point(
1857 (text_bounds.size.width - size.width) / 2.,
1858 text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
1859 );
1860
1861 let origin = text_bounds.origin + offset;
1862 element.prepaint_at(origin, window, cx);
1863 Some((element, origin))
1864 } else {
1865 self.render_edit_prediction_end_of_line_popover(
1866 "Jump to Edit",
1867 editor_snapshot,
1868 visible_row_range,
1869 target_display_point,
1870 line_height,
1871 scroll_pixel_position,
1872 content_origin,
1873 editor_width,
1874 window,
1875 cx,
1876 )
1877 }
1878 }
1879
1880 fn render_edit_prediction_end_of_line_popover(
1881 self: &mut Editor,
1882 label: &'static str,
1883 editor_snapshot: &EditorSnapshot,
1884 visible_row_range: Range<DisplayRow>,
1885 target_display_point: DisplayPoint,
1886 line_height: Pixels,
1887 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
1888 content_origin: gpui::Point<Pixels>,
1889 editor_width: Pixels,
1890 window: &mut Window,
1891 cx: &mut App,
1892 ) -> Option<(AnyElement, gpui::Point<Pixels>)> {
1893 let target_line_end = DisplayPoint::new(
1894 target_display_point.row(),
1895 editor_snapshot.line_len(target_display_point.row()),
1896 );
1897
1898 let mut element = self
1899 .render_edit_prediction_line_popover(label, None, window, cx)
1900 .into_any();
1901
1902 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
1903
1904 let line_origin =
1905 self.display_to_pixel_point(target_line_end, editor_snapshot, window, cx)?;
1906
1907 let start_point = content_origin - point(scroll_pixel_position.x.into(), Pixels::ZERO);
1908 let mut origin = start_point
1909 + line_origin
1910 + point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO);
1911 origin.x = origin.x.max(content_origin.x);
1912
1913 let max_x = content_origin.x + editor_width - size.width;
1914
1915 if origin.x > max_x {
1916 let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y;
1917
1918 let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) {
1919 origin.y += offset;
1920 IconName::ArrowUp
1921 } else {
1922 origin.y -= offset;
1923 IconName::ArrowDown
1924 };
1925
1926 element = self
1927 .render_edit_prediction_line_popover(label, Some(icon), window, cx)
1928 .into_any();
1929
1930 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
1931
1932 origin.x = content_origin.x + editor_width - size.width - px(2.);
1933 }
1934
1935 element.prepaint_at(origin, window, cx);
1936 Some((element, origin))
1937 }
1938
1939 fn render_edit_prediction_diff_popover(
1940 self: &Editor,
1941 text_bounds: &Bounds<Pixels>,
1942 content_origin: gpui::Point<Pixels>,
1943 right_margin: Pixels,
1944 editor_snapshot: &EditorSnapshot,
1945 visible_row_range: Range<DisplayRow>,
1946 line_layouts: &[LineWithInvisibles],
1947 line_height: Pixels,
1948 scroll_position: gpui::Point<ScrollOffset>,
1949 scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
1950 newest_selection_head: Option<DisplayPoint>,
1951 editor_width: Pixels,
1952 style: &EditorStyle,
1953 edits: &Vec<(Range<Anchor>, Arc<str>)>,
1954 edit_preview: &Option<language::EditPreview>,
1955 snapshot: &language::BufferSnapshot,
1956 window: &mut Window,
1957 cx: &mut App,
1958 ) -> Option<(AnyElement, gpui::Point<Pixels>)> {
1959 let Some((first_edit_range, _)) = edits.first() else {
1960 return None;
1961 };
1962 let Some((last_edit_range, _)) = edits.last() else {
1963 return None;
1964 };
1965
1966 let edit_start = first_edit_range.start.to_display_point(editor_snapshot);
1967 let edit_end = last_edit_range.end.to_display_point(editor_snapshot);
1968
1969 let is_visible = visible_row_range.contains(&edit_start.row())
1970 || visible_row_range.contains(&edit_end.row());
1971 if !is_visible {
1972 return None;
1973 }
1974
1975 let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() {
1976 edit_prediction_edit_text(
1977 snapshot,
1978 edits,
1979 edit_preview,
1980 false,
1981 editor_snapshot.buffer_snapshot(),
1982 cx,
1983 )
1984 } else {
1985 // Fallback for providers without edit_preview
1986 edit_prediction_fallback_text(edits, cx)
1987 };
1988
1989 let styled_text = highlighted_edits.to_styled_text(&style.text);
1990 let line_count = highlighted_edits.text.lines().count();
1991
1992 const BORDER_WIDTH: Pixels = px(1.);
1993
1994 let keybind = self.render_edit_prediction_keybind(window, cx);
1995 let has_keybind = keybind.is_some();
1996
1997 let mut element = h_flex()
1998 .items_start()
1999 .debug_selector(|| "edit_prediction_diff_popover".into())
2000 .child(
2001 h_flex()
2002 .bg(cx.theme().colors().editor_background)
2003 .border(BORDER_WIDTH)
2004 .shadow_xs()
2005 .border_color(cx.theme().colors().border)
2006 .rounded_l_lg()
2007 .when(line_count > 1, |el| el.rounded_br_lg())
2008 .pr_1()
2009 .child(styled_text),
2010 )
2011 .child(
2012 h_flex()
2013 .h(line_height + BORDER_WIDTH * 2.)
2014 .px_1p5()
2015 .gap_1()
2016 // Workaround: For some reason, there's a gap if we don't do this
2017 .ml(-BORDER_WIDTH)
2018 .shadow(vec![
2019 gpui::BoxShadow::new(px(1.), px(1.), gpui::black().opacity(0.05))
2020 .blur_radius(px(2.)),
2021 ])
2022 .bg(Editor::edit_prediction_line_popover_bg_color(cx))
2023 .border(BORDER_WIDTH)
2024 .border_color(cx.theme().colors().border)
2025 .rounded_r_lg()
2026 .id("edit_prediction_diff_popover_keybind")
2027 .when(!has_keybind, |el| {
2028 let status_colors = cx.theme().status();
2029
2030 el.bg(status_colors.error_background)
2031 .border_color(status_colors.error.opacity(0.6))
2032 .child(Icon::new(IconName::Info).color(Color::Error))
2033 .cursor_default()
2034 .hoverable_tooltip(move |_window, cx| {
2035 cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
2036 })
2037 })
2038 .children(keybind),
2039 )
2040 .into_any();
2041
2042 let longest_row =
2043 editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
2044 let longest_line_width = if visible_row_range.contains(&longest_row) {
2045 line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
2046 } else {
2047 layout_line(
2048 longest_row,
2049 editor_snapshot,
2050 style,
2051 editor_width,
2052 |_| false,
2053 window,
2054 cx,
2055 )
2056 .width
2057 };
2058
2059 let viewport_bounds =
2060 Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
2061 right: -right_margin,
2062 ..Default::default()
2063 });
2064 let popover_right_bound = cmp::min(text_bounds.right(), viewport_bounds.right());
2065
2066 let x_after_longest = Pixels::from(
2067 ScrollPixelOffset::from(
2068 text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X,
2069 ) - scroll_pixel_position.x,
2070 );
2071
2072 let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
2073
2074 let can_position_to_the_right = x_after_longest < text_bounds.right()
2075 && element_bounds.width <= popover_right_bound - text_bounds.left();
2076
2077 let mut origin = if can_position_to_the_right {
2078 point(
2079 x_after_longest.min(popover_right_bound - element_bounds.width),
2080 text_bounds.origin.y
2081 + Pixels::from(
2082 edit_start.row().as_f64() * ScrollPixelOffset::from(line_height)
2083 - scroll_pixel_position.y,
2084 ),
2085 )
2086 } else {
2087 let cursor_row = newest_selection_head.map(|head| head.row());
2088 let above_edit = edit_start
2089 .row()
2090 .0
2091 .checked_sub(line_count as u32)
2092 .map(DisplayRow);
2093 let below_edit = Some(edit_end.row() + 1);
2094 let above_cursor =
2095 cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
2096 let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
2097
2098 // Place the edit popover adjacent to the edit if there is a location
2099 // available that is onscreen and does not obscure the cursor. Otherwise,
2100 // place it adjacent to the cursor.
2101 let row_target = [above_edit, below_edit, above_cursor, below_cursor]
2102 .into_iter()
2103 .flatten()
2104 .find(|&start_row| {
2105 let end_row = start_row + line_count as u32;
2106 visible_row_range.contains(&start_row)
2107 && visible_row_range.contains(&end_row)
2108 && cursor_row
2109 .is_none_or(|cursor_row| !((start_row..end_row).contains(&cursor_row)))
2110 })?;
2111
2112 content_origin
2113 + point(
2114 Pixels::from(-scroll_pixel_position.x),
2115 Pixels::from(
2116 (row_target.as_f64() - scroll_position.y) * f64::from(line_height),
2117 ),
2118 )
2119 };
2120
2121 origin.x -= BORDER_WIDTH;
2122
2123 window.with_content_mask(
2124 Some(gpui::ContentMask {
2125 bounds: *text_bounds,
2126 }),
2127 |window| {
2128 window.defer_draw(element, origin, 1, Some(window.content_mask()));
2129 },
2130 );
2131
2132 // Do not return an element, since it will already be drawn due to defer_draw.
2133 None
2134 }
2135
2136 fn render_edit_prediction_inline_keystroke(
2137 &self,
2138 keystroke: &gpui::KeybindingKeystroke,
2139 modifiers_color: Color,
2140 cx: &App,
2141 ) -> AnyElement {
2142 let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
2143
2144 h_flex()
2145 .px_0p5()
2146 .when(is_platform_style_mac, |parent| parent.gap_0p5())
2147 .font(
2148 theme_settings::ThemeSettings::get_global(cx)
2149 .buffer_font
2150 .clone(),
2151 )
2152 .text_size(TextSize::XSmall.rems(cx))
2153 .child(h_flex().children(ui::render_modifiers(
2154 keystroke.modifiers(),
2155 PlatformStyle::platform(),
2156 Some(modifiers_color),
2157 Some(IconSize::XSmall.rems().into()),
2158 true,
2159 )))
2160 .when(is_platform_style_mac, |parent| {
2161 parent.child(keystroke.key().to_string())
2162 })
2163 .when(!is_platform_style_mac, |parent| {
2164 parent.child(
2165 Key::new(ui::utils::capitalize(keystroke.key()), Some(Color::Default))
2166 .size(Some(IconSize::XSmall.rems().into())),
2167 )
2168 })
2169 .into_any()
2170 }
2171
2172 fn render_edit_prediction_popover_keystroke(
2173 &self,
2174 keystroke: &gpui::KeybindingKeystroke,
2175 color: Color,
2176 cx: &App,
2177 ) -> AnyElement {
2178 let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
2179
2180 if keystroke.modifiers().modified() {
2181 h_flex()
2182 .font(
2183 theme_settings::ThemeSettings::get_global(cx)
2184 .buffer_font
2185 .clone(),
2186 )
2187 .when(is_platform_style_mac, |parent| parent.gap_1())
2188 .child(h_flex().children(ui::render_modifiers(
2189 keystroke.modifiers(),
2190 PlatformStyle::platform(),
2191 Some(color),
2192 None,
2193 false,
2194 )))
2195 .into_any()
2196 } else {
2197 Key::new(ui::utils::capitalize(keystroke.key()), Some(color))
2198 .size(Some(IconSize::XSmall.rems().into()))
2199 .into_any_element()
2200 }
2201 }
2202
2203 fn render_edit_prediction_keybind(
2204 &self,
2205 window: &mut Window,
2206 cx: &mut App,
2207 ) -> Option<AnyElement> {
2208 let keybind_display =
2209 self.edit_prediction_keybind_display(EditPredictionKeybindSurface::Inline, window, cx);
2210 let keystroke = keybind_display.displayed_keystroke.as_ref()?;
2211
2212 let modifiers_color = if *keystroke.modifiers() == window.modifiers() {
2213 Color::Accent
2214 } else {
2215 Color::Muted
2216 };
2217
2218 Some(self.render_edit_prediction_inline_keystroke(keystroke, modifiers_color, cx))
2219 }
2220
2221 fn render_edit_prediction_line_popover(
2222 &self,
2223 label: impl Into<SharedString>,
2224 icon: Option<IconName>,
2225 window: &mut Window,
2226 cx: &mut App,
2227 ) -> Stateful<Div> {
2228 let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
2229
2230 let keybind = self.render_edit_prediction_keybind(window, cx);
2231 let has_keybind = keybind.is_some();
2232 let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
2233
2234 h_flex()
2235 .id("ep-line-popover")
2236 .py_0p5()
2237 .pl_1()
2238 .pr(padding_right)
2239 .gap_1()
2240 .rounded_md()
2241 .border_1()
2242 .bg(Self::edit_prediction_line_popover_bg_color(cx))
2243 .border_color(Self::edit_prediction_callout_popover_border_color(cx))
2244 .shadow_xs()
2245 .when(!has_keybind, |el| {
2246 let status_colors = cx.theme().status();
2247
2248 el.bg(status_colors.error_background)
2249 .border_color(status_colors.error.opacity(0.6))
2250 .pl_2()
2251 .child(Icon::new(icons.error).color(Color::Error))
2252 .cursor_default()
2253 .hoverable_tooltip(move |_window, cx| {
2254 cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
2255 })
2256 })
2257 .children(keybind)
2258 .child(
2259 Label::new(label)
2260 .size(LabelSize::Small)
2261 .when(!has_keybind, |el| {
2262 el.color(cx.theme().status().error.into()).strikethrough()
2263 }),
2264 )
2265 .when(!has_keybind, |el| {
2266 el.child(
2267 h_flex().ml_1().child(
2268 Icon::new(IconName::Info)
2269 .size(IconSize::Small)
2270 .color(cx.theme().status().error.into()),
2271 ),
2272 )
2273 })
2274 .when_some(icon, |element, icon| {
2275 element.child(
2276 div()
2277 .mt(px(1.5))
2278 .child(Icon::new(icon).size(IconSize::Small)),
2279 )
2280 })
2281 }
2282
2283 fn render_edit_prediction_jump_outside_popover(
2284 &self,
2285 snapshot: &BufferSnapshot,
2286 window: &mut Window,
2287 cx: &mut App,
2288 ) -> Stateful<Div> {
2289 let keybind = self.render_edit_prediction_keybind(window, cx);
2290 let has_keybind = keybind.is_some();
2291 let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
2292
2293 let file_name = snapshot
2294 .file()
2295 .map(|file| SharedString::new(file.file_name(cx)))
2296 .unwrap_or(SharedString::new_static(MultiBuffer::DEFAULT_TITLE));
2297
2298 h_flex()
2299 .id("ep-jump-outside-popover")
2300 .py_1()
2301 .px_2()
2302 .gap_1()
2303 .rounded_md()
2304 .border_1()
2305 .bg(Self::edit_prediction_line_popover_bg_color(cx))
2306 .border_color(Self::edit_prediction_callout_popover_border_color(cx))
2307 .shadow_xs()
2308 .when(!has_keybind, |el| {
2309 let status_colors = cx.theme().status();
2310
2311 el.bg(status_colors.error_background)
2312 .border_color(status_colors.error.opacity(0.6))
2313 .pl_2()
2314 .child(Icon::new(icons.error).color(Color::Error))
2315 .cursor_default()
2316 .hoverable_tooltip(move |_window, cx| {
2317 cx.new(|_| MissingEditPredictionKeybindingTooltip).into()
2318 })
2319 })
2320 .children(keybind)
2321 .child(
2322 Label::new(file_name)
2323 .size(LabelSize::Small)
2324 .buffer_font(cx)
2325 .when(!has_keybind, |el| {
2326 el.color(cx.theme().status().error.into()).strikethrough()
2327 }),
2328 )
2329 .when(!has_keybind, |el| {
2330 el.child(
2331 h_flex().ml_1().child(
2332 Icon::new(IconName::Info)
2333 .size(IconSize::Small)
2334 .color(cx.theme().status().error.into()),
2335 ),
2336 )
2337 })
2338 .child(
2339 div()
2340 .mt(px(1.5))
2341 .child(Icon::new(IconName::ArrowUpRight).size(IconSize::Small)),
2342 )
2343 }
2344
2345 fn edit_prediction_line_popover_bg_color(cx: &App) -> Hsla {
2346 let accent_color = cx.theme().colors().text_accent;
2347 let editor_bg_color = cx.theme().colors().editor_background;
2348 editor_bg_color.blend(accent_color.opacity(0.1))
2349 }
2350
2351 fn edit_prediction_callout_popover_border_color(cx: &App) -> Hsla {
2352 let accent_color = cx.theme().colors().text_accent;
2353 let editor_bg_color = cx.theme().colors().editor_background;
2354 editor_bg_color.blend(accent_color.opacity(0.6))
2355 }
2356
2357 fn get_prediction_provider_icons(
2358 provider: &Option<RegisteredEditPredictionDelegate>,
2359 cx: &App,
2360 ) -> edit_prediction_types::EditPredictionIconSet {
2361 match provider {
2362 Some(provider) => provider.provider.icons(cx),
2363 None => edit_prediction_types::EditPredictionIconSet::new(IconName::OmegaPredict),
2364 }
2365 }
2366
2367 fn render_edit_prediction_cursor_popover_preview(
2368 &self,
2369 completion: &EditPredictionState,
2370 cursor_point: Point,
2371 style: &EditorStyle,
2372 cx: &mut Context<Editor>,
2373 ) -> Option<Div> {
2374 use text::ToPoint as _;
2375
2376 fn render_relative_row_jump(
2377 prefix: impl Into<String>,
2378 current_row: u32,
2379 target_row: u32,
2380 ) -> Div {
2381 let (row_diff, arrow) = if target_row < current_row {
2382 (current_row - target_row, IconName::ArrowUp)
2383 } else {
2384 (target_row - current_row, IconName::ArrowDown)
2385 };
2386
2387 h_flex()
2388 .child(
2389 Label::new(format!("{}{}", prefix.into(), row_diff))
2390 .color(Color::Muted)
2391 .size(LabelSize::Small),
2392 )
2393 .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small))
2394 }
2395
2396 let supports_jump = self
2397 .edit_prediction_provider
2398 .as_ref()
2399 .map(|provider| provider.provider.supports_jump_to_edit())
2400 .unwrap_or(true);
2401
2402 let icons = Self::get_prediction_provider_icons(&self.edit_prediction_provider, cx);
2403
2404 match &completion.completion {
2405 EditPrediction::MoveWithin {
2406 target, snapshot, ..
2407 } => {
2408 if !supports_jump {
2409 return None;
2410 }
2411 let (target, _) = self.display_snapshot(cx).anchor_to_buffer_anchor(*target)?;
2412
2413 Some(
2414 h_flex()
2415 .px_2()
2416 .gap_2()
2417 .flex_1()
2418 .child(if target.to_point(snapshot).row > cursor_point.row {
2419 Icon::new(icons.down)
2420 } else {
2421 Icon::new(icons.up)
2422 })
2423 .child(Label::new("Jump to Edit")),
2424 )
2425 }
2426 EditPrediction::MoveOutside { snapshot, .. } => {
2427 let file_name = snapshot
2428 .file()
2429 .map(|file| file.file_name(cx))
2430 .unwrap_or(MultiBuffer::DEFAULT_TITLE);
2431 Some(
2432 h_flex()
2433 .px_2()
2434 .gap_2()
2435 .flex_1()
2436 .child(Icon::new(icons.base))
2437 .child(Label::new(format!("Jump to {file_name}"))),
2438 )
2439 }
2440 EditPrediction::Edit {
2441 edits,
2442 edit_preview,
2443 snapshot,
2444 ..
2445 } => {
2446 let first_edit_row = self
2447 .display_snapshot(cx)
2448 .anchor_to_buffer_anchor(edits.first()?.0.start)?
2449 .0
2450 .to_point(snapshot)
2451 .row;
2452
2453 let (highlighted_edits, has_more_lines) =
2454 if let Some(edit_preview) = edit_preview.as_ref() {
2455 edit_prediction_edit_text(
2456 snapshot,
2457 edits,
2458 edit_preview,
2459 true,
2460 &self.display_snapshot(cx),
2461 cx,
2462 )
2463 .first_line_preview()
2464 } else {
2465 edit_prediction_fallback_text(edits, cx).first_line_preview()
2466 };
2467
2468 let styled_text = gpui::StyledText::new(highlighted_edits.text)
2469 .with_default_highlights(&style.text, highlighted_edits.highlights);
2470
2471 let preview = h_flex()
2472 .gap_1()
2473 .min_w_16()
2474 .child(styled_text)
2475 .when(has_more_lines, |parent| parent.child("…"));
2476
2477 let left = if supports_jump && first_edit_row != cursor_point.row {
2478 render_relative_row_jump("", cursor_point.row, first_edit_row)
2479 .into_any_element()
2480 } else {
2481 Icon::new(icons.base).into_any_element()
2482 };
2483
2484 Some(
2485 h_flex()
2486 .h_full()
2487 .flex_1()
2488 .gap_2()
2489 .pr_1()
2490 .overflow_x_hidden()
2491 .font(
2492 theme_settings::ThemeSettings::get_global(cx)
2493 .buffer_font
2494 .clone(),
2495 )
2496 .child(left)
2497 .child(preview),
2498 )
2499 }
2500 }
2501 }
2502}
2503
2504#[cfg(test)]
2505impl Editor {
2506 pub(super) fn set_menu_edit_predictions_policy(&mut self, value: MenuEditPredictionsPolicy) {
2507 self.menu_edit_predictions_policy = value;
2508 }
2509}
2510
2511struct MissingEditPredictionKeybindingTooltip;
2512
2513impl Render for MissingEditPredictionKeybindingTooltip {
2514 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2515 ui::tooltip_container(cx, |container, cx| {
2516 container
2517 .flex_shrink_0()
2518 .max_w_80()
2519 .min_h(rems_from_px(124.))
2520 .justify_between()
2521 .child(
2522 v_flex()
2523 .flex_1()
2524 .text_ui_sm(cx)
2525 .child(Label::new("Conflict with Accept Keybinding"))
2526 .child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.")
2527 )
2528 .child(
2529 h_flex()
2530 .pb_1()
2531 .gap_1()
2532 .items_end()
2533 .w_full()
2534 .child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
2535 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx)
2536 }))
2537 .child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
2538 cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");
2539 })),
2540 )
2541 })
2542 }
2543}
2544
2545fn edit_prediction_fallback_text(edits: &[(Range<Anchor>, Arc<str>)], cx: &App) -> HighlightedText {
2546 // Fallback for providers that don't provide edit_preview (like Copilot)
2547 // Just show the raw edit text with basic styling
2548 let mut text = String::new();
2549 let mut highlights = Vec::new();
2550
2551 let insertion_highlight_style = HighlightStyle {
2552 color: Some(cx.theme().colors().text),
2553 ..Default::default()
2554 };
2555
2556 for (_, edit_text) in edits {
2557 let start_offset = text.len();
2558 text.push_str(edit_text);
2559 let end_offset = text.len();
2560
2561 if start_offset < end_offset {
2562 highlights.push((start_offset..end_offset, insertion_highlight_style));
2563 }
2564 }
2565
2566 HighlightedText {
2567 text: text.into(),
2568 highlights,
2569 }
2570}
2571
2572fn all_edits_insertions_or_deletions(
2573 edits: &Vec<(Range<Anchor>, Arc<str>)>,
2574 snapshot: &MultiBufferSnapshot,
2575) -> bool {
2576 let mut all_insertions = true;
2577 let mut all_deletions = true;
2578
2579 for (range, new_text) in edits.iter() {
2580 let range_is_empty = range.to_offset(snapshot).is_empty();
2581 let text_is_empty = new_text.is_empty();
2582
2583 if range_is_empty != text_is_empty {
2584 if range_is_empty {
2585 all_deletions = false;
2586 } else {
2587 all_insertions = false;
2588 }
2589 } else {
2590 return false;
2591 }
2592
2593 if !all_insertions && !all_deletions {
2594 return false;
2595 }
2596 }
2597 all_insertions || all_deletions
2598}
2599