Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:33:23.669Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

edit_prediction_tests.rs

1904 lines · 65.4 KB · rust
1use edit_prediction_types::{
2    EditPredictionDelegate, EditPredictionIconSet, EditPredictionRequestTrigger,
3    PredictedCursorPosition,
4};
5use futures::StreamExt;
6use gpui::{
7    Entity, Focusable, KeyBinding, KeybindingKeystroke, Keystroke, Modifiers, NoAction, Pixels,
8    Task, prelude::*, size,
9};
10use indoc::indoc;
11use language::EditPredictionsMode;
12use language::{Buffer, CodeLabel};
13use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
14use project::{Completion, CompletionResponse, CompletionSource};
15use std::{
16    ops::Range,
17    path::PathBuf,
18    rc::Rc,
19    sync::{
20        Arc,
21        atomic::{self, AtomicUsize},
22    },
23};
24use text::{Point, ToOffset};
25use ui::prelude::*;
26
27use crate::{
28    AcceptEditPrediction, CodeContextMenu, CompletionContext, CompletionProvider, EditPrediction,
29    EditPredictionKeybindAction, EditPredictionKeybindSurface, MenuEditPredictionsPolicy,
30    MultiBuffer, ShowCompletions,
31    editor_tests::{init_test, update_test_language_settings},
32    test::{
33        build_editor, editor_lsp_test_context::EditorLspTestContext,
34        editor_test_context::EditorTestContext,
35    },
36};
37use rpc::proto::PeerId;
38use workspace::CollaboratorId;
39
40struct EditorWithRightOccluders {
41    editor: Entity<crate::Editor>,
42    editor_width: Pixels,
43    right_dock_width: Option<Pixels>,
44    right_sidebar_width: Option<Pixels>,
45}
46
47impl Render for EditorWithRightOccluders {
48    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
49        h_flex()
50            .size_full()
51            .child(
52                div()
53                    .h_full()
54                    .w(self.editor_width)
55                    .overflow_hidden()
56                    .child(self.editor.clone()),
57            )
58            .when_some(self.right_dock_width, |this, width| {
59                this.child(
60                    div()
61                        .h_full()
62                        .w(width)
63                        .flex_shrink_0()
64                        .occlude()
65                        .debug_selector(|| "right_dock".into()),
66                )
67            })
68            .when_some(self.right_sidebar_width, |this, width| {
69                this.child(
70                    div()
71                        .h_full()
72                        .w(width)
73                        .flex_shrink_0()
74                        .occlude()
75                        .debug_selector(|| "right_sidebar".into()),
76                )
77            })
78    }
79}
80
81async fn assert_edit_prediction_diff_popover_avoids_right_occluders(
82    cx: &mut gpui::TestAppContext,
83    right_dock_width: Option<Pixels>,
84    right_sidebar_width: Option<Pixels>,
85) {
86    init_test(cx, |_| {});
87
88    let editor_width = px(700.);
89    let window_width = editor_width
90        + right_dock_width.unwrap_or_default()
91        + right_sidebar_width.unwrap_or_default();
92    let buffer = cx.update(|cx| MultiBuffer::build_simple("", cx));
93    let window = cx.add_window(|window, cx| {
94        let editor = cx.new(|cx| build_editor(buffer, window, cx));
95        window.focus(&editor.focus_handle(cx), cx);
96        EditorWithRightOccluders {
97            editor,
98            editor_width,
99            right_dock_width,
100            right_sidebar_width,
101        }
102    });
103    let editor = window
104        .read_with(cx, |root, _| root.editor.clone())
105        .expect("test window should contain editor");
106    let mut cx = gpui::VisualTestContext::from_window(*window, cx);
107    cx.simulate_resize(size(window_width, px(500.)));
108    cx.run_until_parked();
109
110    let mut cx = EditorTestContext::for_editor_in(editor, &mut cx).await;
111    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
112    assign_editor_completion_provider(provider.clone(), &mut cx);
113    cx.update_editor(|editor, _, _| {
114        editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);
115    });
116    cx.set_state("abcdefghijklmnopqrstuvwxyzabcdefghijklmnˇopqrstuvwxyzabcdef");
117
118    propose_edits(&provider, vec![(40..41, "REPLACEMENT")], &mut cx);
119    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
120    cx.editor(|editor, _, _| {
121        assert!(!editor.edit_prediction_visible_in_cursor_popover(true));
122        assert!(matches!(
123            editor
124                .active_edit_prediction
125                .as_ref()
126                .map(|state| &state.completion),
127            Some(EditPrediction::Edit {
128                display_mode: crate::EditDisplayMode::DiffPopover,
129                ..
130            })
131        ));
132    });
133    cx.cx.update(|window, cx| {
134        window.refresh();
135        let _ = window.draw(cx);
136    });
137
138    cx.editor(|editor, _, _| {
139        assert!(
140            editor.last_position_map.is_some(),
141            "editor should have rendered a position map"
142        );
143    });
144
145    let popover_bounds = cx
146        .cx
147        .debug_bounds("edit_prediction_diff_popover")
148        .expect("diff popover should render");
149
150    for selector in ["right_dock", "right_sidebar"] {
151        if let Some(occluder_bounds) = cx.cx.debug_bounds(selector) {
152            assert!(
153                !popover_bounds.intersects(&occluder_bounds),
154                "diff popover {popover_bounds:?} should not overlap {selector} {occluder_bounds:?}"
155            );
156        }
157    }
158}
159
160#[gpui::test]
161async fn test_edit_prediction_diff_popover_avoids_right_sidebar(cx: &mut gpui::TestAppContext) {
162    assert_edit_prediction_diff_popover_avoids_right_occluders(cx, None, Some(px(300.))).await;
163}
164
165#[gpui::test]
166async fn test_edit_prediction_diff_popover_avoids_right_dock(cx: &mut gpui::TestAppContext) {
167    assert_edit_prediction_diff_popover_avoids_right_occluders(cx, Some(px(300.)), None).await;
168}
169
170#[gpui::test]
171async fn test_edit_prediction_diff_popover_avoids_right_dock_and_sidebar(
172    cx: &mut gpui::TestAppContext,
173) {
174    assert_edit_prediction_diff_popover_avoids_right_occluders(cx, Some(px(300.)), Some(px(300.)))
175        .await;
176}
177
178#[gpui::test]
179async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) {
180    init_test(cx, |_| {});
181
182    let mut cx = EditorTestContext::new(cx).await;
183    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
184    assign_editor_completion_provider(provider.clone(), &mut cx);
185    cx.set_state("let absolute_zero_celsius = ˇ;");
186
187    propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
188    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
189
190    assert_editor_active_edit_completion(&mut cx, |_, edits| {
191        assert_eq!(edits.len(), 1);
192        assert_eq!(edits[0].1.as_ref(), "-273.15");
193    });
194
195    accept_completion(&mut cx);
196
197    cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;")
198}
199
200#[gpui::test]
201async fn test_edit_prediction_cursor_position_inside_insertion(cx: &mut gpui::TestAppContext) {
202    init_test(cx, |_| {
203        eprintln!("");
204    });
205
206    let mut cx = EditorTestContext::new(cx).await;
207    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
208
209    assign_editor_completion_provider(provider.clone(), &mut cx);
210    // Buffer: "fn foo() {}" - we'll insert text and position cursor inside the insertion
211    cx.set_state("fn foo() ˇ{}");
212
213    // Insert "bar()" at offset 9, with cursor at offset 2 within the insertion (after "ba")
214    // This tests the case where cursor is inside newly inserted text
215    propose_edits_with_cursor_position_in_insertion(
216        &provider,
217        vec![(9..9, "bar()")],
218        9, // anchor at the insertion point
219        2, // offset 2 within "bar()" puts cursor after "ba"
220        &mut cx,
221    );
222    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
223
224    assert_editor_active_edit_completion(&mut cx, |_, edits| {
225        assert_eq!(edits.len(), 1);
226        assert_eq!(edits[0].1.as_ref(), "bar()");
227    });
228
229    accept_completion(&mut cx);
230
231    // Cursor should be inside the inserted text at "baˇr()"
232    cx.assert_editor_state("fn foo() baˇr(){}");
233}
234
235#[gpui::test]
236async fn test_edit_prediction_cursor_position_outside_edit(cx: &mut gpui::TestAppContext) {
237    init_test(cx, |_| {});
238
239    let mut cx = EditorTestContext::new(cx).await;
240    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
241    assign_editor_completion_provider(provider.clone(), &mut cx);
242    // Buffer: "let x = ;" with cursor before semicolon - we'll insert "42" and position cursor elsewhere
243    cx.set_state("let x = ˇ;");
244
245    // Insert "42" at offset 8, but set cursor_position to offset 4 (the 'x')
246    // This tests that cursor moves to the predicted position, not the end of the edit
247    propose_edits_with_cursor_position(
248        &provider,
249        vec![(8..8, "42")],
250        Some(4), // cursor at offset 4 (the 'x'), NOT at the edit location
251        &mut cx,
252    );
253    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
254
255    assert_editor_active_edit_completion(&mut cx, |_, edits| {
256        assert_eq!(edits.len(), 1);
257        assert_eq!(edits[0].1.as_ref(), "42");
258    });
259
260    accept_completion(&mut cx);
261
262    // Cursor should be at offset 4 (the 'x'), not at the end of the inserted "42"
263    cx.assert_editor_state("let ˇx = 42;");
264}
265
266#[gpui::test]
267async fn test_edit_prediction_cursor_position_fallback(cx: &mut gpui::TestAppContext) {
268    init_test(cx, |_| {});
269
270    let mut cx = EditorTestContext::new(cx).await;
271    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
272    assign_editor_completion_provider(provider.clone(), &mut cx);
273    cx.set_state("let x = ˇ;");
274
275    // Propose an edit without a cursor position - should fall back to end of edit
276    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
277    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
278
279    accept_completion(&mut cx);
280
281    // Cursor should be at the end of the inserted text (default behavior)
282    cx.assert_editor_state("let x = 42ˇ;")
283}
284
285#[gpui::test]
286async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) {
287    init_test(cx, |_| {});
288
289    let mut cx = EditorTestContext::new(cx).await;
290    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
291    assign_editor_completion_provider(provider.clone(), &mut cx);
292    cx.set_state("let pi = ˇ\"foo\";");
293
294    propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
295    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
296
297    assert_editor_active_edit_completion(&mut cx, |_, edits| {
298        assert_eq!(edits.len(), 1);
299        assert_eq!(edits[0].1.as_ref(), "3.14159");
300    });
301
302    accept_completion(&mut cx);
303
304    cx.assert_editor_state("let pi = 3.14159ˇ;")
305}
306
307#[gpui::test]
308async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) {
309    init_test(cx, |_| {});
310
311    let mut cx = EditorTestContext::new(cx).await;
312    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
313    assign_editor_completion_provider(provider.clone(), &mut cx);
314
315    // Cursor is 2+ lines above the proposed edit
316    cx.set_state(indoc! {"
317        line 0
318        line ˇ1
319        line 2
320        line 3
321        line
322    "});
323
324    propose_edits(
325        &provider,
326        vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
327        &mut cx,
328    );
329
330    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
331    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
332        assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
333    });
334
335    // When accepting, cursor is moved to the proposed location
336    accept_completion(&mut cx);
337    cx.assert_editor_state(indoc! {"
338        line 0
339        line 1
340        line 2
341        line 3
342        linˇe
343    "});
344
345    // Cursor is 2+ lines below the proposed edit
346    cx.set_state(indoc! {"
347        line 0
348        line
349        line 2
350        line 3
351        line ˇ4
352    "});
353
354    propose_edits(
355        &provider,
356        vec![(Point::new(1, 3)..Point::new(1, 3), " 1")],
357        &mut cx,
358    );
359
360    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
361    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
362        assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
363    });
364
365    // When accepting, cursor is moved to the proposed location
366    accept_completion(&mut cx);
367    cx.assert_editor_state(indoc! {"
368        line 0
369        linˇe
370        line 2
371        line 3
372        line 4
373    "});
374}
375
376#[gpui::test]
377async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) {
378    init_test(cx, |_| {});
379
380    let mut cx = EditorTestContext::new(cx).await;
381    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
382    assign_editor_completion_provider(provider.clone(), &mut cx);
383
384    // Cursor is 3+ lines above the proposed edit
385    cx.set_state(indoc! {"
386        line 0
387        line ˇ1
388        line 2
389        line 3
390        line 4
391        line
392    "});
393    let edit_location = Point::new(5, 3);
394
395    propose_edits(
396        &provider,
397        vec![(edit_location..edit_location, " 5")],
398        &mut cx,
399    );
400
401    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
402    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
403        assert_eq!(move_target.to_point(&snapshot), edit_location);
404    });
405
406    // If we move *towards* the completion, it stays active
407    cx.set_selections_state(indoc! {"
408        line 0
409        line 1
410        line ˇ2
411        line 3
412        line 4
413        line
414    "});
415    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
416        assert_eq!(move_target.to_point(&snapshot), edit_location);
417    });
418
419    // If we move *away* from the completion, it is discarded
420    cx.set_selections_state(indoc! {"
421        line ˇ0
422        line 1
423        line 2
424        line 3
425        line 4
426        line
427    "});
428    cx.editor(|editor, _, _| {
429        assert!(editor.active_edit_prediction.is_none());
430    });
431
432    // Cursor is 3+ lines below the proposed edit
433    cx.set_state(indoc! {"
434        line
435        line 1
436        line 2
437        line 3
438        line ˇ4
439        line 5
440    "});
441    let edit_location = Point::new(0, 3);
442
443    propose_edits(
444        &provider,
445        vec![(edit_location..edit_location, " 0")],
446        &mut cx,
447    );
448
449    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
450    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
451        assert_eq!(move_target.to_point(&snapshot), edit_location);
452    });
453
454    // If we move *towards* the completion, it stays active
455    cx.set_selections_state(indoc! {"
456        line
457        line 1
458        line 2
459        line ˇ3
460        line 4
461        line 5
462    "});
463    assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
464        assert_eq!(move_target.to_point(&snapshot), edit_location);
465    });
466
467    // If we move *away* from the completion, it is discarded
468    cx.set_selections_state(indoc! {"
469        line
470        line 1
471        line 2
472        line 3
473        line 4
474        line ˇ5
475    "});
476    cx.editor(|editor, _, _| {
477        assert!(editor.active_edit_prediction.is_none());
478    });
479}
480
481#[gpui::test]
482async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui::TestAppContext) {
483    init_test(cx, |_| {});
484
485    let mut cx = EditorTestContext::new(cx).await;
486    let provider = cx.new(|_| FakeNonZedEditPredictionDelegate::default());
487    assign_editor_completion_provider_non_zed(provider.clone(), &mut cx);
488
489    // Cursor is 2+ lines above the proposed edit
490    cx.set_state(indoc! {"
491        line 0
492        line ˇ1
493        line 2
494        line 3
495        line
496    "});
497
498    propose_edits_non_zed(
499        &provider,
500        vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
501        &mut cx,
502    );
503
504    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
505
506    // For non-Zed providers, there should be no move completion (jump functionality disabled)
507    cx.editor(|editor, _, _| {
508        if let Some(completion_state) = &editor.active_edit_prediction {
509            // Should be an Edit prediction, not a Move prediction
510            match &completion_state.completion {
511                EditPrediction::Edit { .. } => {
512                    // This is expected for non-Zed providers
513                }
514                EditPrediction::MoveWithin { .. } | EditPrediction::MoveOutside { .. } => {
515                    panic!(
516                        "Non-Zed providers should not show Move predictions (jump functionality)"
517                    );
518                }
519            }
520        }
521    });
522}
523
524#[gpui::test]
525async fn test_edit_prediction_refresh_suppressed_while_following(cx: &mut gpui::TestAppContext) {
526    init_test(cx, |_| {});
527
528    let mut cx = EditorTestContext::new(cx).await;
529    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
530    assign_editor_completion_provider(provider.clone(), &mut cx);
531    cx.set_state("let x = ˇ;");
532
533    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
534
535    cx.update_editor(|editor, window, cx| {
536        editor.refresh_edit_prediction(
537            false,
538            false,
539            EditPredictionRequestTrigger::Other,
540            window,
541            cx,
542        );
543        editor.update_visible_edit_prediction(window, cx);
544    });
545
546    assert_eq!(
547        provider.read_with(&cx.cx, |provider, _| {
548            provider.refresh_count.load(atomic::Ordering::SeqCst)
549        }),
550        1
551    );
552    cx.editor(|editor, _, _| {
553        assert!(editor.active_edit_prediction.is_some());
554    });
555
556    cx.update_editor(|editor, window, cx| {
557        editor.leader_id = Some(CollaboratorId::PeerId(PeerId::default()));
558        editor.refresh_edit_prediction(
559            false,
560            false,
561            EditPredictionRequestTrigger::Other,
562            window,
563            cx,
564        );
565    });
566
567    assert_eq!(
568        provider.read_with(&cx.cx, |provider, _| {
569            provider.refresh_count.load(atomic::Ordering::SeqCst)
570        }),
571        1
572    );
573    cx.editor(|editor, _, _| {
574        assert!(editor.active_edit_prediction.is_none());
575    });
576
577    cx.update_editor(|editor, window, cx| {
578        editor.leader_id = None;
579        editor.refresh_edit_prediction(
580            false,
581            false,
582            EditPredictionRequestTrigger::Other,
583            window,
584            cx,
585        );
586    });
587
588    assert_eq!(
589        provider.read_with(&cx.cx, |provider, _| {
590            provider.refresh_count.load(atomic::Ordering::SeqCst)
591        }),
592        2
593    );
594}
595
596#[gpui::test]
597async fn test_edit_prediction_preview_cleanup_on_toggle_off(cx: &mut gpui::TestAppContext) {
598    init_test(cx, |_| {});
599
600    // Bind `ctrl-shift-a` to accept the provided edit prediction. The actual key
601    // binding here doesn't matter, we simply need to confirm that holding the
602    // binding's modifiers triggers the edit prediction preview.
603    cx.update(|cx| cx.bind_keys([KeyBinding::new("ctrl-shift-a", AcceptEditPrediction, None)]));
604
605    let mut cx = EditorTestContext::new(cx).await;
606    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
607    assign_editor_completion_provider(provider.clone(), &mut cx);
608    cx.set_state("let x = ˇ;");
609
610    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
611    cx.update_editor(|editor, window, cx| {
612        editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider);
613        editor.update_visible_edit_prediction(window, cx)
614    });
615
616    cx.editor(|editor, _, _| {
617        assert!(editor.has_active_edit_prediction());
618    });
619
620    // Simulate pressing the modifiers for `AcceptEditPrediction`, namely
621    // `ctrl-shift`, so that we can confirm that the edit prediction preview is
622    // activated.
623    let modifiers = Modifiers::control_shift();
624    cx.simulate_modifiers_change(modifiers);
625    cx.run_until_parked();
626
627    cx.editor(|editor, _, _| {
628        assert!(editor.edit_prediction_preview_is_active());
629    });
630
631    // Disable showing edit predictions without issuing a new modifiers changed
632    // event, to confirm that the edit prediction preview is still active.
633    cx.update_editor(|editor, window, cx| {
634        editor.set_show_edit_predictions(Some(false), window, cx);
635    });
636
637    cx.editor(|editor, _, _| {
638        assert!(!editor.has_active_edit_prediction());
639        assert!(editor.edit_prediction_preview_is_active());
640    });
641
642    // Now release the modifiers
643    // Simulate releasing all modifiers, ensuring that even with edit prediction
644    // disabled, the edit prediction preview is cleaned up.
645    cx.simulate_modifiers_change(Modifiers::none());
646    cx.run_until_parked();
647
648    cx.editor(|editor, _, _| {
649        assert!(!editor.edit_prediction_preview_is_active());
650    });
651}
652
653#[gpui::test]
654async fn test_hidden_edit_prediction_does_not_open_snippet_menu_on_word_input(
655    cx: &mut gpui::TestAppContext,
656) {
657    init_test(cx, |_| {});
658
659    let mut cx = hidden_edit_prediction_snippet_test_context(cx).await;
660    cx.simulate_input("t");
661    cx.run_until_parked();
662
663    cx.update_editor(|editor, _, _| {
664        assert!(editor.has_active_edit_prediction());
665        assert!(editor.context_menu.borrow().is_none());
666    });
667}
668
669#[gpui::test]
670async fn test_hidden_edit_prediction_opens_snippet_menu_for_strong_prefix_match(
671    cx: &mut gpui::TestAppContext,
672) {
673    init_test(cx, |_| {});
674
675    let mut cx = hidden_edit_prediction_snippet_test_context(cx).await;
676    cx.simulate_input("t");
677    cx.run_until_parked();
678    cx.simulate_input("h");
679    cx.run_until_parked();
680
681    cx.update_editor(|editor, _, _| {
682        let Some(CodeContextMenu::Completions(menu)) = &*editor.context_menu.borrow() else {
683            panic!("expected completions menu");
684        };
685        let entries = menu.entries.borrow();
686        assert!(
687            entries
688                .iter()
689                .any(|entry| { entry.as_match().is_some_and(|m| m.string == "Theta") })
690        );
691    });
692}
693
694#[gpui::test]
695async fn test_edit_prediction_preview_activates_when_prediction_arrives_with_modifier_held(
696    cx: &mut gpui::TestAppContext,
697) {
698    init_test(cx, |_| {});
699    load_default_keymap(cx);
700    update_test_language_settings(cx, &|settings| {
701        settings.edit_predictions.get_or_insert_default().mode = Some(EditPredictionsMode::Subtle);
702    });
703
704    let mut cx = EditorTestContext::new(cx).await;
705    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
706    assign_editor_completion_provider(provider.clone(), &mut cx);
707    cx.set_state("let x = ˇ;");
708
709    cx.editor(|editor, _, _| {
710        assert!(!editor.has_active_edit_prediction());
711        assert!(!editor.edit_prediction_preview_is_active());
712    });
713
714    let preview_modifiers = cx.update_editor(|editor, window, cx| {
715        *editor
716            .preview_edit_prediction_keystroke(window, cx)
717            .unwrap()
718            .modifiers()
719    });
720
721    cx.simulate_modifiers_change(preview_modifiers);
722    cx.run_until_parked();
723
724    cx.editor(|editor, _, _| {
725        assert!(!editor.has_active_edit_prediction());
726        assert!(editor.edit_prediction_preview_is_active());
727    });
728
729    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
730    cx.update_editor(|editor, window, cx| {
731        editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider);
732        editor.update_visible_edit_prediction(window, cx)
733    });
734
735    cx.editor(|editor, _, _| {
736        assert!(editor.has_active_edit_prediction());
737        assert!(
738            editor.edit_prediction_preview_is_active(),
739            "prediction preview should activate immediately when the prediction arrives while the preview modifier is still held",
740        );
741    });
742}
743
744#[gpui::test]
745async fn test_edit_prediction_preview_does_not_hide_code_actions_on_modifier_press(
746    cx: &mut gpui::TestAppContext,
747) {
748    init_test(cx, |_| {});
749    update_test_language_settings(cx, &|settings| {
750        settings.edit_predictions.get_or_insert_default().mode = Some(EditPredictionsMode::Subtle);
751    });
752    cx.update(|cx| {
753        cx.bind_keys([KeyBinding::new(
754            "ctrl-enter",
755            AcceptEditPrediction,
756            Some("Editor && edit_prediction && !showing_completions"),
757        )]);
758    });
759
760    let mut cx = EditorLspTestContext::new_rust(
761        lsp::ServerCapabilities {
762            code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
763            ..Default::default()
764        },
765        cx,
766    )
767    .await;
768    cx.set_state(indoc! {"
769        fn main() {
770            let valueˇ = 1;
771        }
772    "});
773
774    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
775    cx.update_editor(|editor, window, cx| {
776        editor.set_edit_prediction_provider(
777            Some(provider.clone()),
778            EditPredictionRequestTrigger::EditorCreated,
779            window,
780            cx,
781        );
782    });
783
784    let snapshot = cx.buffer_snapshot();
785    let edit_position = snapshot.anchor_after(Point::new(1, 13));
786    cx.update(|_, cx| {
787        provider.update(cx, |provider, _| {
788            provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
789                id: None,
790                edits: vec![(edit_position..edit_position, " + 1".into())],
791                cursor_position: None,
792                edit_preview: None,
793            }))
794        })
795    });
796    cx.update_editor(|editor, window, cx| {
797        editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider);
798        editor.update_visible_edit_prediction(window, cx);
799    });
800    cx.update_editor(|editor, _, _| {
801        assert!(editor.has_active_edit_prediction());
802        assert!(editor.stale_edit_prediction_in_menu.is_none());
803    });
804
805    let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
806        move |_, _, _| async move {
807            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
808                lsp::CodeAction {
809                    title: "Inline value".to_string(),
810                    kind: Some(lsp::CodeActionKind::QUICKFIX),
811                    ..Default::default()
812                },
813            )]))
814        },
815    );
816
817    cx.update_editor(|editor, window, cx| {
818        editor.toggle_code_actions(
819            &crate::actions::ToggleCodeActions {
820                deployed_from: None,
821                quick_launch: false,
822            },
823            window,
824            cx,
825        );
826    });
827    code_action_requests.next().await;
828    cx.run_until_parked();
829    cx.condition(|editor, _| editor.context_menu_visible())
830        .await;
831
832    cx.update_editor(|editor, _, _| {
833        assert!(!editor.has_active_edit_prediction());
834        assert!(editor.stale_edit_prediction_in_menu.is_some());
835        assert!(editor.context_menu_visible());
836        assert!(matches!(
837            editor.context_menu.borrow().as_ref(),
838            Some(crate::code_context_menus::CodeContextMenu::CodeActions(_))
839        ));
840        assert!(!editor.edit_prediction_preview_is_active());
841    });
842
843    cx.simulate_modifiers_change(Modifiers::control());
844    cx.run_until_parked();
845
846    cx.update_editor(|editor, _, _| {
847        assert!(
848            !editor.edit_prediction_preview_is_active(),
849            "modifier-only press should not activate edit prediction preview while code actions are open"
850        );
851        assert!(
852            editor.context_menu_visible(),
853            "modifier-only press should not hide the code actions menu"
854        );
855        assert!(matches!(
856            editor.context_menu.borrow().as_ref(),
857            Some(crate::code_context_menus::CodeContextMenu::CodeActions(_))
858        ));
859    });
860}
861
862#[gpui::test]
863async fn test_edit_prediction_preview_supersedes_completions_menu(cx: &mut gpui::TestAppContext) {
864    init_test(cx, |_| {});
865    update_test_language_settings(cx, &|settings| {
866        settings.edit_predictions.get_or_insert_default().mode = Some(EditPredictionsMode::Subtle);
867    });
868    cx.update(|cx| {
869        cx.bind_keys([KeyBinding::new(
870            "ctrl-enter",
871            AcceptEditPrediction,
872            Some("Editor && edit_prediction && showing_completions"),
873        )]);
874    });
875
876    let mut cx = EditorTestContext::new(cx).await;
877    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
878    assign_editor_completion_provider(provider.clone(), &mut cx);
879    assign_editor_completion_menu_provider(&mut cx);
880    cx.set_state("let x = ˇ;");
881
882    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
883    cx.update_editor(|editor, window, cx| {
884        editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider);
885        editor.update_visible_edit_prediction(window, cx);
886    });
887    cx.update_editor(|editor, window, cx| {
888        editor.show_completions(&ShowCompletions, window, cx);
889    });
890    cx.run_until_parked();
891
892    cx.editor(|editor, _, _| {
893        assert!(editor.has_active_edit_prediction());
894        assert!(editor.context_menu_visible());
895        assert!(matches!(
896            editor.context_menu.borrow().as_ref(),
897            Some(crate::code_context_menus::CodeContextMenu::Completions(_))
898        ));
899        assert!(!editor.edit_prediction_preview_is_active());
900    });
901
902    cx.simulate_modifiers_change(Modifiers::control());
903    cx.run_until_parked();
904
905    cx.editor(|editor, _, _| {
906        assert!(editor.edit_prediction_preview_is_active());
907        assert!(!editor.context_menu_visible());
908        assert!(matches!(
909            editor.context_menu.borrow().as_ref(),
910            Some(crate::code_context_menus::CodeContextMenu::Completions(_))
911        ));
912    });
913}
914
915fn load_default_keymap(cx: &mut gpui::TestAppContext) {
916    cx.update(|cx| {
917        cx.bind_keys(
918            settings::KeymapFile::load_asset_allow_partial_failure(
919                settings::DEFAULT_KEYMAP_PATH,
920                cx,
921            )
922            .expect("failed to load default keymap"),
923        );
924    });
925}
926
927#[gpui::test]
928async fn test_inline_edit_prediction_keybind_selection_cases(cx: &mut gpui::TestAppContext) {
929    enum InlineKeybindState {
930        Normal,
931        ShowingCompletions,
932        InLeadingWhitespace,
933        ShowingCompletionsAndLeadingWhitespace,
934    }
935
936    enum ExpectedKeystroke {
937        DefaultAccept,
938        DefaultPreview,
939        Literal(&'static str),
940    }
941
942    struct InlineKeybindCase {
943        name: &'static str,
944        use_default_keymap: bool,
945        mode: EditPredictionsMode,
946        extra_bindings: Vec<KeyBinding>,
947        state: InlineKeybindState,
948        expected_accept_keystroke: ExpectedKeystroke,
949        expected_preview_keystroke: ExpectedKeystroke,
950        expected_displayed_keystroke: ExpectedKeystroke,
951    }
952
953    init_test(cx, |_| {});
954    load_default_keymap(cx);
955    let mut default_cx = EditorTestContext::new(cx).await;
956    let provider = default_cx.new(|_| FakeEditPredictionDelegate::default());
957    assign_editor_completion_provider(provider.clone(), &mut default_cx);
958    default_cx.set_state("let x = ˇ;");
959    propose_edits(&provider, vec![(8..8, "42")], &mut default_cx);
960    default_cx
961        .update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
962
963    let (default_accept_keystroke, default_preview_keystroke) =
964        default_cx.update_editor(|editor, window, cx| {
965            let keybind_display = editor.edit_prediction_keybind_display(
966                EditPredictionKeybindSurface::Inline,
967                window,
968                cx,
969            );
970            let accept_keystroke = keybind_display
971                .accept_keystroke
972                .as_ref()
973                .expect("default inline edit prediction should have an accept binding")
974                .clone();
975            let preview_keystroke = keybind_display
976                .preview_keystroke
977                .as_ref()
978                .expect("default inline edit prediction should have a preview binding")
979                .clone();
980            (accept_keystroke, preview_keystroke)
981        });
982
983    let cases = [
984        InlineKeybindCase {
985            name: "default setup prefers tab over alt-tab for accept",
986            use_default_keymap: true,
987            mode: EditPredictionsMode::Eager,
988            extra_bindings: Vec::new(),
989            state: InlineKeybindState::Normal,
990            expected_accept_keystroke: ExpectedKeystroke::DefaultAccept,
991            expected_preview_keystroke: ExpectedKeystroke::DefaultPreview,
992            expected_displayed_keystroke: ExpectedKeystroke::DefaultAccept,
993        },
994        InlineKeybindCase {
995            name: "subtle mode displays preview binding inline",
996            use_default_keymap: true,
997            mode: EditPredictionsMode::Subtle,
998            extra_bindings: Vec::new(),
999            state: InlineKeybindState::Normal,
1000            expected_accept_keystroke: ExpectedKeystroke::DefaultPreview,
1001            expected_preview_keystroke: ExpectedKeystroke::DefaultPreview,
1002            expected_displayed_keystroke: ExpectedKeystroke::DefaultPreview,
1003        },
1004        InlineKeybindCase {
1005            name: "removing default tab binding still displays tab",
1006            use_default_keymap: true,
1007            mode: EditPredictionsMode::Eager,
1008            extra_bindings: vec![KeyBinding::new(
1009                "tab",
1010                NoAction,
1011                Some("Editor && edit_prediction && edit_prediction_mode == eager"),
1012            )],
1013            state: InlineKeybindState::Normal,
1014            expected_accept_keystroke: ExpectedKeystroke::DefaultPreview,
1015            expected_preview_keystroke: ExpectedKeystroke::DefaultPreview,
1016            expected_displayed_keystroke: ExpectedKeystroke::DefaultPreview,
1017        },
1018        InlineKeybindCase {
1019            name: "custom-only rebound accept key uses replacement key",
1020            use_default_keymap: true,
1021            mode: EditPredictionsMode::Eager,
1022            extra_bindings: vec![KeyBinding::new(
1023                "ctrl-enter",
1024                AcceptEditPrediction,
1025                Some("Editor && edit_prediction"),
1026            )],
1027            state: InlineKeybindState::Normal,
1028            expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1029            expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1030            expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1031        },
1032        InlineKeybindCase {
1033            name: "showing completions restores conflict-context binding",
1034            use_default_keymap: true,
1035            mode: EditPredictionsMode::Eager,
1036            extra_bindings: vec![KeyBinding::new(
1037                "ctrl-enter",
1038                AcceptEditPrediction,
1039                Some("Editor && edit_prediction && showing_completions"),
1040            )],
1041            state: InlineKeybindState::ShowingCompletions,
1042            expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1043            expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1044            expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1045        },
1046        InlineKeybindCase {
1047            name: "leading whitespace restores conflict-context binding",
1048            use_default_keymap: false,
1049            mode: EditPredictionsMode::Eager,
1050            extra_bindings: vec![KeyBinding::new(
1051                "ctrl-enter",
1052                AcceptEditPrediction,
1053                Some("Editor && edit_prediction && in_leading_whitespace"),
1054            )],
1055            state: InlineKeybindState::InLeadingWhitespace,
1056            expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1057            expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1058            expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1059        },
1060        InlineKeybindCase {
1061            name: "showing completions and leading whitespace restore combined conflict binding",
1062            use_default_keymap: false,
1063            mode: EditPredictionsMode::Eager,
1064            extra_bindings: vec![KeyBinding::new(
1065                "ctrl-enter",
1066                AcceptEditPrediction,
1067                Some("Editor && edit_prediction && showing_completions && in_leading_whitespace"),
1068            )],
1069            state: InlineKeybindState::ShowingCompletionsAndLeadingWhitespace,
1070            expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1071            expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1072            expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"),
1073        },
1074    ];
1075
1076    for case in cases {
1077        init_test(cx, |_| {});
1078        if case.use_default_keymap {
1079            load_default_keymap(cx);
1080        }
1081        update_test_language_settings(cx, &|settings| {
1082            settings.edit_predictions.get_or_insert_default().mode = Some(case.mode);
1083        });
1084
1085        if !case.extra_bindings.is_empty() {
1086            cx.update(|cx| cx.bind_keys(case.extra_bindings.clone()));
1087        }
1088
1089        let mut cx = EditorTestContext::new(cx).await;
1090        let provider = cx.new(|_| FakeEditPredictionDelegate::default());
1091        assign_editor_completion_provider(provider.clone(), &mut cx);
1092
1093        match case.state {
1094            InlineKeybindState::Normal | InlineKeybindState::ShowingCompletions => {
1095                cx.set_state("let x = ˇ;");
1096            }
1097            InlineKeybindState::InLeadingWhitespace
1098            | InlineKeybindState::ShowingCompletionsAndLeadingWhitespace => {
1099                cx.set_state(indoc! {"
1100                    fn main() {
1101                        ˇ
1102                    }
1103                "});
1104            }
1105        }
1106
1107        propose_edits(&provider, vec![(8..8, "42")], &mut cx);
1108        cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
1109
1110        if matches!(
1111            case.state,
1112            InlineKeybindState::ShowingCompletions
1113                | InlineKeybindState::ShowingCompletionsAndLeadingWhitespace
1114        ) {
1115            assign_editor_completion_menu_provider(&mut cx);
1116            cx.update_editor(|editor, window, cx| {
1117                editor.show_completions(&ShowCompletions, window, cx);
1118            });
1119            cx.run_until_parked();
1120        }
1121
1122        cx.update_editor(|editor, window, cx| {
1123            assert!(
1124                editor.has_active_edit_prediction(),
1125                "case '{}' should have an active edit prediction",
1126                case.name
1127            );
1128
1129            let keybind_display = editor.edit_prediction_keybind_display(
1130                EditPredictionKeybindSurface::Inline,
1131                window,
1132                cx,
1133            );
1134            let accept_keystroke = keybind_display
1135                .accept_keystroke
1136                .as_ref()
1137                .unwrap_or_else(|| panic!("case '{}' should have an accept binding", case.name));
1138            let preview_keystroke = keybind_display
1139                .preview_keystroke
1140                .as_ref()
1141                .unwrap_or_else(|| panic!("case '{}' should have a preview binding", case.name));
1142            let displayed_keystroke = keybind_display
1143                .displayed_keystroke
1144                .as_ref()
1145                .unwrap_or_else(|| panic!("case '{}' should have a displayed binding", case.name));
1146
1147            let expected_accept_keystroke = match case.expected_accept_keystroke {
1148                ExpectedKeystroke::DefaultAccept => default_accept_keystroke.clone(),
1149                ExpectedKeystroke::DefaultPreview => default_preview_keystroke.clone(),
1150                ExpectedKeystroke::Literal(keystroke) => KeybindingKeystroke::from_keystroke(
1151                    Keystroke::parse(keystroke).expect("expected test keystroke to parse"),
1152                ),
1153            };
1154            let expected_preview_keystroke = match case.expected_preview_keystroke {
1155                ExpectedKeystroke::DefaultAccept => default_accept_keystroke.clone(),
1156                ExpectedKeystroke::DefaultPreview => default_preview_keystroke.clone(),
1157                ExpectedKeystroke::Literal(keystroke) => KeybindingKeystroke::from_keystroke(
1158                    Keystroke::parse(keystroke).expect("expected test keystroke to parse"),
1159                ),
1160            };
1161            let expected_displayed_keystroke = match case.expected_displayed_keystroke {
1162                ExpectedKeystroke::DefaultAccept => default_accept_keystroke.clone(),
1163                ExpectedKeystroke::DefaultPreview => default_preview_keystroke.clone(),
1164                ExpectedKeystroke::Literal(keystroke) => KeybindingKeystroke::from_keystroke(
1165                    Keystroke::parse(keystroke).expect("expected test keystroke to parse"),
1166                ),
1167            };
1168
1169            assert_eq!(
1170                accept_keystroke, &expected_accept_keystroke,
1171                "case '{}' selected the wrong accept binding",
1172                case.name
1173            );
1174            assert_eq!(
1175                preview_keystroke, &expected_preview_keystroke,
1176                "case '{}' selected the wrong preview binding",
1177                case.name
1178            );
1179            assert_eq!(
1180                displayed_keystroke, &expected_displayed_keystroke,
1181                "case '{}' selected the wrong displayed binding",
1182                case.name
1183            );
1184
1185            if matches!(case.mode, EditPredictionsMode::Subtle) {
1186                assert!(
1187                    editor.edit_prediction_requires_modifier(),
1188                    "case '{}' should require a modifier",
1189                    case.name
1190                );
1191            }
1192        });
1193    }
1194}
1195
1196#[gpui::test]
1197async fn test_tab_accepts_edit_prediction_over_completion(cx: &mut gpui::TestAppContext) {
1198    init_test(cx, |_| {});
1199    load_default_keymap(cx);
1200
1201    let mut cx = EditorTestContext::new(cx).await;
1202    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
1203    assign_editor_completion_provider(provider.clone(), &mut cx);
1204    cx.set_state("let x = ˇ;");
1205
1206    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
1207    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
1208
1209    assert_editor_active_edit_completion(&mut cx, |_, edits| {
1210        assert_eq!(edits.len(), 1);
1211        assert_eq!(edits[0].1.as_ref(), "42");
1212    });
1213
1214    cx.simulate_keystroke("tab");
1215    cx.run_until_parked();
1216
1217    cx.assert_editor_state("let x = 42ˇ;");
1218}
1219
1220#[gpui::test]
1221async fn test_cursor_popover_edit_prediction_keybind_cases(cx: &mut gpui::TestAppContext) {
1222    enum CursorPopoverPredictionKind {
1223        SingleLine,
1224        MultiLine,
1225        SingleLineWithPreview,
1226        MultiLineWithPreview,
1227        DeleteSingleNewline,
1228        StaleSingleLineAfterMultiLine,
1229    }
1230
1231    struct CursorPopoverCase {
1232        name: &'static str,
1233        prediction_kind: CursorPopoverPredictionKind,
1234        expected_action: EditPredictionKeybindAction,
1235    }
1236
1237    let cases = [
1238        CursorPopoverCase {
1239            name: "single line prediction uses accept action",
1240            prediction_kind: CursorPopoverPredictionKind::SingleLine,
1241            expected_action: EditPredictionKeybindAction::Accept,
1242        },
1243        CursorPopoverCase {
1244            name: "multi line prediction uses preview action",
1245            prediction_kind: CursorPopoverPredictionKind::MultiLine,
1246            expected_action: EditPredictionKeybindAction::Preview,
1247        },
1248        CursorPopoverCase {
1249            name: "single line prediction with preview still uses accept action",
1250            prediction_kind: CursorPopoverPredictionKind::SingleLineWithPreview,
1251            expected_action: EditPredictionKeybindAction::Accept,
1252        },
1253        CursorPopoverCase {
1254            name: "multi line prediction with preview uses preview action",
1255            prediction_kind: CursorPopoverPredictionKind::MultiLineWithPreview,
1256            expected_action: EditPredictionKeybindAction::Preview,
1257        },
1258        CursorPopoverCase {
1259            name: "single line newline deletion uses accept action",
1260            prediction_kind: CursorPopoverPredictionKind::DeleteSingleNewline,
1261            expected_action: EditPredictionKeybindAction::Accept,
1262        },
1263        CursorPopoverCase {
1264            name: "stale multi line prediction does not force preview action",
1265            prediction_kind: CursorPopoverPredictionKind::StaleSingleLineAfterMultiLine,
1266            expected_action: EditPredictionKeybindAction::Accept,
1267        },
1268    ];
1269
1270    for case in cases {
1271        init_test(cx, |_| {});
1272        load_default_keymap(cx);
1273
1274        let mut cx = EditorTestContext::new(cx).await;
1275        let provider = cx.new(|_| FakeEditPredictionDelegate::default());
1276        assign_editor_completion_provider(provider.clone(), &mut cx);
1277
1278        match case.prediction_kind {
1279            CursorPopoverPredictionKind::SingleLine => {
1280                cx.set_state("let x = ˇ;");
1281                propose_edits(&provider, vec![(8..8, "42")], &mut cx);
1282                cx.update_editor(|editor, window, cx| {
1283                    editor.update_visible_edit_prediction(window, cx)
1284                });
1285            }
1286            CursorPopoverPredictionKind::MultiLine => {
1287                cx.set_state("let x = ˇ;");
1288                propose_edits(&provider, vec![(8..8, "42\n43")], &mut cx);
1289                cx.update_editor(|editor, window, cx| {
1290                    editor.update_visible_edit_prediction(window, cx)
1291                });
1292            }
1293            CursorPopoverPredictionKind::SingleLineWithPreview => {
1294                cx.set_state("let x = ˇ;");
1295                propose_edits_with_preview(&provider, vec![(8..8, "42")], &mut cx).await;
1296                cx.update_editor(|editor, window, cx| {
1297                    editor.update_visible_edit_prediction(window, cx)
1298                });
1299            }
1300            CursorPopoverPredictionKind::MultiLineWithPreview => {
1301                cx.set_state("let x = ˇ;");
1302                propose_edits_with_preview(&provider, vec![(8..8, "42\n43")], &mut cx).await;
1303                cx.update_editor(|editor, window, cx| {
1304                    editor.update_visible_edit_prediction(window, cx)
1305                });
1306            }
1307            CursorPopoverPredictionKind::DeleteSingleNewline => {
1308                cx.set_state(indoc! {"
1309                    fn main() {
1310                        let value = 1;
1311                        ˇprintln!(\"done\");
1312                    }
1313                "});
1314                propose_edits(
1315                    &provider,
1316                    vec![(Point::new(1, 18)..Point::new(2, 17), "")],
1317                    &mut cx,
1318                );
1319                cx.update_editor(|editor, window, cx| {
1320                    editor.update_visible_edit_prediction(window, cx)
1321                });
1322            }
1323            CursorPopoverPredictionKind::StaleSingleLineAfterMultiLine => {
1324                cx.set_state("let x = ˇ;");
1325                propose_edits(&provider, vec![(8..8, "42\n43")], &mut cx);
1326                cx.update_editor(|editor, window, cx| {
1327                    editor.update_visible_edit_prediction(window, cx)
1328                });
1329                cx.update_editor(|editor, _window, cx| {
1330                    assert!(editor.active_edit_prediction.is_some());
1331                    assert!(editor.stale_edit_prediction_in_menu.is_none());
1332                    editor.take_active_edit_prediction(true, cx);
1333                    assert!(editor.active_edit_prediction.is_none());
1334                    assert!(editor.stale_edit_prediction_in_menu.is_some());
1335                });
1336
1337                propose_edits(&provider, vec![(8..8, "42")], &mut cx);
1338                cx.update_editor(|editor, window, cx| {
1339                    editor.update_visible_edit_prediction(window, cx)
1340                });
1341            }
1342        }
1343
1344        cx.update_editor(|editor, window, cx| {
1345            assert!(
1346                editor.has_active_edit_prediction(),
1347                "case '{}' should have an active edit prediction",
1348                case.name
1349            );
1350
1351            let keybind_display = editor.edit_prediction_keybind_display(
1352                EditPredictionKeybindSurface::CursorPopoverExpanded,
1353                window,
1354                cx,
1355            );
1356            let accept_keystroke = keybind_display
1357                .accept_keystroke
1358                .as_ref()
1359                .unwrap_or_else(|| panic!("case '{}' should have an accept binding", case.name));
1360            let preview_keystroke = keybind_display
1361                .preview_keystroke
1362                .as_ref()
1363                .unwrap_or_else(|| panic!("case '{}' should have a preview binding", case.name));
1364
1365            assert_eq!(
1366                keybind_display.action, case.expected_action,
1367                "case '{}' selected the wrong cursor popover action",
1368                case.name
1369            );
1370            assert_eq!(
1371                accept_keystroke.key(),
1372                "tab",
1373                "case '{}' selected the wrong accept binding",
1374                case.name
1375            );
1376            assert!(
1377                preview_keystroke.modifiers().modified(),
1378                "case '{}' should use a modified preview binding",
1379                case.name
1380            );
1381
1382            if matches!(
1383                case.prediction_kind,
1384                CursorPopoverPredictionKind::StaleSingleLineAfterMultiLine
1385            ) {
1386                assert!(
1387                    editor.stale_edit_prediction_in_menu.is_none(),
1388                    "case '{}' should clear stale menu state",
1389                    case.name
1390                );
1391            }
1392        });
1393    }
1394}
1395
1396fn assert_editor_active_edit_completion(
1397    cx: &mut EditorTestContext,
1398    assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, Arc<str>)>),
1399) {
1400    cx.editor(|editor, _, cx| {
1401        let completion_state = editor
1402            .active_edit_prediction
1403            .as_ref()
1404            .expect("editor has no active completion");
1405
1406        if let EditPrediction::Edit { edits, .. } = &completion_state.completion {
1407            assert(editor.buffer().read(cx).snapshot(cx), edits);
1408        } else {
1409            panic!("expected edit completion");
1410        }
1411    })
1412}
1413
1414fn assert_editor_active_move_completion(
1415    cx: &mut EditorTestContext,
1416    assert: impl FnOnce(MultiBufferSnapshot, Anchor),
1417) {
1418    cx.editor(|editor, _, cx| {
1419        let completion_state = editor
1420            .active_edit_prediction
1421            .as_ref()
1422            .expect("editor has no active completion");
1423
1424        if let EditPrediction::MoveWithin { target, .. } = &completion_state.completion {
1425            assert(editor.buffer().read(cx).snapshot(cx), *target);
1426        } else {
1427            panic!("expected move completion");
1428        }
1429    })
1430}
1431
1432#[gpui::test]
1433async fn test_cancel_clears_stale_edit_prediction_in_menu(cx: &mut gpui::TestAppContext) {
1434    init_test(cx, |_| {});
1435    load_default_keymap(cx);
1436
1437    let mut cx = EditorTestContext::new(cx).await;
1438    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
1439    assign_editor_completion_provider(provider.clone(), &mut cx);
1440    cx.set_state("let x = ˇ;");
1441
1442    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
1443    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
1444
1445    cx.update_editor(|editor, _window, _cx| {
1446        assert!(editor.active_edit_prediction.is_some());
1447        assert!(editor.stale_edit_prediction_in_menu.is_none());
1448    });
1449
1450    cx.simulate_keystroke("escape");
1451    cx.run_until_parked();
1452
1453    cx.update_editor(|editor, _window, _cx| {
1454        assert!(editor.active_edit_prediction.is_none());
1455        assert!(editor.stale_edit_prediction_in_menu.is_none());
1456    });
1457}
1458
1459#[gpui::test]
1460async fn test_discard_clears_delegate_completion(cx: &mut gpui::TestAppContext) {
1461    init_test(cx, |_| {});
1462    load_default_keymap(cx);
1463
1464    let mut cx = EditorTestContext::new(cx).await;
1465    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
1466    assign_editor_completion_provider(provider.clone(), &mut cx);
1467    cx.set_state("let x = ˇ;");
1468
1469    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
1470    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
1471
1472    cx.update_editor(|editor, _window, _cx| {
1473        assert!(editor.active_edit_prediction.is_some());
1474    });
1475
1476    // Dismiss the prediction — this must call discard() on the delegate,
1477    // which should clear self.completion.
1478    cx.simulate_keystroke("escape");
1479    cx.run_until_parked();
1480
1481    cx.update_editor(|editor, _window, _cx| {
1482        assert!(editor.active_edit_prediction.is_none());
1483    });
1484
1485    // update_visible_edit_prediction must NOT bring the prediction back,
1486    // because discard() cleared self.completion in the delegate.
1487    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
1488
1489    cx.update_editor(|editor, _window, _cx| {
1490        assert!(
1491            editor.active_edit_prediction.is_none(),
1492            "prediction must not resurface after discard()"
1493        );
1494    });
1495}
1496
1497fn accept_completion(cx: &mut EditorTestContext) {
1498    cx.update_editor(|editor, window, cx| {
1499        editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
1500    })
1501}
1502
1503fn propose_edits<T: ToOffset>(
1504    provider: &Entity<FakeEditPredictionDelegate>,
1505    edits: Vec<(Range<T>, &str)>,
1506    cx: &mut EditorTestContext,
1507) {
1508    propose_edits_with_cursor_position(provider, edits, None, cx);
1509}
1510
1511async fn propose_edits_with_preview<T: ToOffset + Clone>(
1512    provider: &Entity<FakeEditPredictionDelegate>,
1513    edits: Vec<(Range<T>, &str)>,
1514    cx: &mut EditorTestContext,
1515) {
1516    let snapshot = cx.buffer_snapshot();
1517    let edits = edits
1518        .into_iter()
1519        .map(|(range, text)| {
1520            let anchor_range =
1521                snapshot.anchor_after(range.start.clone())..snapshot.anchor_before(range.end);
1522            (anchor_range, Arc::<str>::from(text))
1523        })
1524        .collect::<Vec<_>>();
1525
1526    let preview_edits = edits
1527        .iter()
1528        .map(|(range, text)| (range.clone(), text.clone()))
1529        .collect::<Arc<[_]>>();
1530
1531    let edit_preview = cx
1532        .buffer(|buffer: &Buffer, app| buffer.preview_edits(preview_edits, app))
1533        .await;
1534
1535    let provider_edits = edits.into_iter().collect();
1536
1537    cx.update(|_, cx| {
1538        provider.update(cx, |provider, _| {
1539            provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
1540                id: None,
1541                edits: provider_edits,
1542                cursor_position: None,
1543                edit_preview: Some(edit_preview),
1544            }))
1545        })
1546    });
1547}
1548
1549fn propose_edits_with_cursor_position<T: ToOffset>(
1550    provider: &Entity<FakeEditPredictionDelegate>,
1551    edits: Vec<(Range<T>, &str)>,
1552    cursor_offset: Option<usize>,
1553    cx: &mut EditorTestContext,
1554) {
1555    let snapshot = cx.buffer_snapshot();
1556    let cursor_position = cursor_offset
1557        .map(|offset| PredictedCursorPosition::at_anchor(snapshot.anchor_after(offset)));
1558    let edits = edits.into_iter().map(|(range, text)| {
1559        let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
1560        (range, text.into())
1561    });
1562
1563    cx.update(|_, cx| {
1564        provider.update(cx, |provider, _| {
1565            provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
1566                id: None,
1567                edits: edits.collect(),
1568                cursor_position,
1569                edit_preview: None,
1570            }))
1571        })
1572    });
1573}
1574
1575fn propose_edits_with_cursor_position_in_insertion<T: ToOffset>(
1576    provider: &Entity<FakeEditPredictionDelegate>,
1577    edits: Vec<(Range<T>, &str)>,
1578    anchor_offset: usize,
1579    offset_within_insertion: usize,
1580    cx: &mut EditorTestContext,
1581) {
1582    let snapshot = cx.buffer_snapshot();
1583    // Use anchor_before (left bias) so the anchor stays at the insertion point
1584    // rather than moving past the inserted text
1585    let cursor_position = Some(PredictedCursorPosition::new(
1586        snapshot.anchor_before(anchor_offset),
1587        offset_within_insertion,
1588    ));
1589    let edits = edits.into_iter().map(|(range, text)| {
1590        let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
1591        (range, text.into())
1592    });
1593
1594    cx.update(|_, cx| {
1595        provider.update(cx, |provider, _| {
1596            provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
1597                id: None,
1598                edits: edits.collect(),
1599                cursor_position,
1600                edit_preview: None,
1601            }))
1602        })
1603    });
1604}
1605
1606async fn hidden_edit_prediction_snippet_test_context(
1607    cx: &mut gpui::TestAppContext,
1608) -> EditorTestContext {
1609    let mut cx = EditorTestContext::new(cx).await;
1610    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
1611    assign_editor_completion_provider(provider.clone(), &mut cx);
1612    cx.update_editor(|editor, _, cx| {
1613        editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);
1614        editor.project().unwrap().update(cx, |project, cx| {
1615            project.snippets().update(cx, |snippets, _cx| {
1616                let snippet = project::snippet_provider::Snippet {
1617                    prefix: vec!["Theta".to_string(), "turnstile".to_string()],
1618                    body: "⊢".to_string(),
1619                    description: Some("unicode symbol".to_string()),
1620                    name: "unicode snippets".to_string(),
1621                };
1622                snippets.add_snippet_for_test(
1623                    None,
1624                    PathBuf::from("test_snippets.json"),
1625                    vec![Arc::new(snippet)],
1626                );
1627            });
1628        })
1629    });
1630    cx.set_state("ˇ");
1631
1632    propose_edits(&provider, vec![(0..0, "x")], &mut cx);
1633    cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
1634    cx
1635}
1636
1637fn assign_editor_completion_provider(
1638    provider: Entity<FakeEditPredictionDelegate>,
1639    cx: &mut EditorTestContext,
1640) {
1641    cx.update_editor(|editor, window, cx| {
1642        editor.set_edit_prediction_provider(
1643            Some(provider),
1644            EditPredictionRequestTrigger::EditorCreated,
1645            window,
1646            cx,
1647        );
1648    })
1649}
1650
1651fn assign_editor_completion_menu_provider(cx: &mut EditorTestContext) {
1652    cx.update_editor(|editor, _, _| {
1653        editor.set_completion_provider(Some(Rc::new(FakeCompletionMenuProvider)));
1654    });
1655}
1656
1657fn propose_edits_non_zed<T: ToOffset>(
1658    provider: &Entity<FakeNonZedEditPredictionDelegate>,
1659    edits: Vec<(Range<T>, &str)>,
1660    cx: &mut EditorTestContext,
1661) {
1662    let snapshot = cx.buffer_snapshot();
1663    let edits = edits.into_iter().map(|(range, text)| {
1664        let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
1665        (range, text.into())
1666    });
1667
1668    cx.update(|_, cx| {
1669        provider.update(cx, |provider, _| {
1670            provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
1671                id: None,
1672                edits: edits.collect(),
1673                cursor_position: None,
1674                edit_preview: None,
1675            }))
1676        })
1677    });
1678}
1679
1680fn assign_editor_completion_provider_non_zed(
1681    provider: Entity<FakeNonZedEditPredictionDelegate>,
1682    cx: &mut EditorTestContext,
1683) {
1684    cx.update_editor(|editor, window, cx| {
1685        editor.set_edit_prediction_provider(
1686            Some(provider),
1687            EditPredictionRequestTrigger::EditorCreated,
1688            window,
1689            cx,
1690        );
1691    })
1692}
1693
1694struct FakeCompletionMenuProvider;
1695
1696impl CompletionProvider for FakeCompletionMenuProvider {
1697    fn completions(
1698        &self,
1699        buffer: &Entity<Buffer>,
1700        _buffer_position: text::Anchor,
1701        _trigger: CompletionContext,
1702        _window: &mut Window,
1703        cx: &mut Context<crate::Editor>,
1704    ) -> Task<anyhow::Result<Vec<CompletionResponse>>> {
1705        let replace_range = text::Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id());
1706        let completions = ["fake_completion", "fake_completion_2"]
1707            .into_iter()
1708            .map(|label| Completion {
1709                replace_range: replace_range.clone(),
1710                new_text: label.to_string(),
1711                label: CodeLabel::plain(label.to_string(), None),
1712                documentation: None,
1713                source: CompletionSource::Custom,
1714                icon_path: None,
1715                icon_color: None,
1716                match_start: None,
1717                snippet_deduplication_key: None,
1718                insert_text_mode: None,
1719                confirm: None,
1720                group: None,
1721            })
1722            .collect();
1723
1724        Task::ready(Ok(vec![CompletionResponse {
1725            completions,
1726            display_options: Default::default(),
1727            is_incomplete: false,
1728        }]))
1729    }
1730
1731    fn is_completion_trigger(
1732        &self,
1733        _buffer: &Entity<Buffer>,
1734        _position: language::Anchor,
1735        _text: &str,
1736        _trigger_in_words: bool,
1737        _cx: &mut Context<crate::Editor>,
1738    ) -> bool {
1739        false
1740    }
1741
1742    fn filter_completions(&self) -> bool {
1743        false
1744    }
1745}
1746
1747#[derive(Default, Clone)]
1748pub struct FakeEditPredictionDelegate {
1749    pub completion: Option<edit_prediction_types::EditPrediction>,
1750    pub refresh_count: Arc<AtomicUsize>,
1751}
1752
1753impl FakeEditPredictionDelegate {
1754    pub fn set_edit_prediction(
1755        &mut self,
1756        completion: Option<edit_prediction_types::EditPrediction>,
1757    ) {
1758        self.completion = completion;
1759    }
1760}
1761
1762impl EditPredictionDelegate for FakeEditPredictionDelegate {
1763    fn name() -> &'static str {
1764        "fake-completion-provider"
1765    }
1766
1767    fn display_name() -> &'static str {
1768        "Fake Completion Provider"
1769    }
1770
1771    fn show_predictions_in_menu() -> bool {
1772        true
1773    }
1774
1775    fn supports_jump_to_edit() -> bool {
1776        true
1777    }
1778
1779    fn icons(&self, _cx: &gpui::App) -> EditPredictionIconSet {
1780        EditPredictionIconSet::new(IconName::OmegaPredict)
1781    }
1782
1783    fn is_enabled(
1784        &self,
1785        _buffer: &gpui::Entity<language::Buffer>,
1786        _cursor_position: language::Anchor,
1787        _cx: &gpui::App,
1788    ) -> bool {
1789        true
1790    }
1791
1792    fn is_refreshing(&self, _cx: &gpui::App) -> bool {
1793        false
1794    }
1795
1796    fn refresh(
1797        &mut self,
1798        _buffer: gpui::Entity<language::Buffer>,
1799        _cursor_position: language::Anchor,
1800        _debounce: bool,
1801        _trigger: edit_prediction_types::EditPredictionRequestTrigger,
1802        _cx: &mut gpui::Context<Self>,
1803    ) {
1804        self.refresh_count.fetch_add(1, atomic::Ordering::SeqCst);
1805    }
1806
1807    fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
1808
1809    fn discard(
1810        &mut self,
1811        _reason: edit_prediction_types::EditPredictionDiscardReason,
1812        _cx: &mut gpui::Context<Self>,
1813    ) {
1814        self.completion.take();
1815    }
1816
1817    fn suggest<'a>(
1818        &mut self,
1819        _buffer: &gpui::Entity<language::Buffer>,
1820        _cursor_position: language::Anchor,
1821        _cx: &mut gpui::Context<Self>,
1822    ) -> Option<edit_prediction_types::EditPrediction> {
1823        self.completion.clone()
1824    }
1825}
1826
1827#[derive(Default, Clone)]
1828pub struct FakeNonZedEditPredictionDelegate {
1829    pub completion: Option<edit_prediction_types::EditPrediction>,
1830}
1831
1832impl FakeNonZedEditPredictionDelegate {
1833    pub fn set_edit_prediction(
1834        &mut self,
1835        completion: Option<edit_prediction_types::EditPrediction>,
1836    ) {
1837        self.completion = completion;
1838    }
1839}
1840
1841impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
1842    fn name() -> &'static str {
1843        "fake-non-zed-provider"
1844    }
1845
1846    fn display_name() -> &'static str {
1847        "Fake Non-Zed Provider"
1848    }
1849
1850    fn show_predictions_in_menu() -> bool {
1851        false
1852    }
1853
1854    fn supports_jump_to_edit() -> bool {
1855        false
1856    }
1857
1858    fn icons(&self, _cx: &gpui::App) -> EditPredictionIconSet {
1859        EditPredictionIconSet::new(IconName::OmegaPredict)
1860    }
1861
1862    fn is_enabled(
1863        &self,
1864        _buffer: &gpui::Entity<language::Buffer>,
1865        _cursor_position: language::Anchor,
1866        _cx: &gpui::App,
1867    ) -> bool {
1868        true
1869    }
1870
1871    fn is_refreshing(&self, _cx: &gpui::App) -> bool {
1872        false
1873    }
1874
1875    fn refresh(
1876        &mut self,
1877        _buffer: gpui::Entity<language::Buffer>,
1878        _cursor_position: language::Anchor,
1879        _debounce: bool,
1880        _trigger: edit_prediction_types::EditPredictionRequestTrigger,
1881        _cx: &mut gpui::Context<Self>,
1882    ) {
1883    }
1884
1885    fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
1886
1887    fn discard(
1888        &mut self,
1889        _reason: edit_prediction_types::EditPredictionDiscardReason,
1890        _cx: &mut gpui::Context<Self>,
1891    ) {
1892        self.completion.take();
1893    }
1894
1895    fn suggest<'a>(
1896        &mut self,
1897        _buffer: &gpui::Entity<language::Buffer>,
1898        _cursor_position: language::Anchor,
1899        _cx: &mut gpui::Context<Self>,
1900    ) -> Option<edit_prediction_types::EditPrediction> {
1901        self.completion.clone()
1902    }
1903}
1904
Served at tenant.openagents/omega Member data and write actions are omitted.