Skip to repository content1904 lines · 65.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:33:23.669Z 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_tests.rs
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