use edit_prediction_types::{ EditPredictionDelegate, EditPredictionIconSet, EditPredictionRequestTrigger, PredictedCursorPosition, }; use futures::StreamExt; use gpui::{ Entity, Focusable, KeyBinding, KeybindingKeystroke, Keystroke, Modifiers, NoAction, Pixels, Task, prelude::*, size, }; use indoc::indoc; use language::EditPredictionsMode; use language::{Buffer, CodeLabel}; use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint}; use project::{Completion, CompletionResponse, CompletionSource}; use std::{ ops::Range, path::PathBuf, rc::Rc, sync::{ Arc, atomic::{self, AtomicUsize}, }, }; use text::{Point, ToOffset}; use ui::prelude::*; use crate::{ AcceptEditPrediction, CodeContextMenu, CompletionContext, CompletionProvider, EditPrediction, EditPredictionKeybindAction, EditPredictionKeybindSurface, MenuEditPredictionsPolicy, MultiBuffer, ShowCompletions, editor_tests::{init_test, update_test_language_settings}, test::{ build_editor, editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext, }, }; use rpc::proto::PeerId; use workspace::CollaboratorId; struct EditorWithRightOccluders { editor: Entity, editor_width: Pixels, right_dock_width: Option, right_sidebar_width: Option, } impl Render for EditorWithRightOccluders { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { h_flex() .size_full() .child( div() .h_full() .w(self.editor_width) .overflow_hidden() .child(self.editor.clone()), ) .when_some(self.right_dock_width, |this, width| { this.child( div() .h_full() .w(width) .flex_shrink_0() .occlude() .debug_selector(|| "right_dock".into()), ) }) .when_some(self.right_sidebar_width, |this, width| { this.child( div() .h_full() .w(width) .flex_shrink_0() .occlude() .debug_selector(|| "right_sidebar".into()), ) }) } } async fn assert_edit_prediction_diff_popover_avoids_right_occluders( cx: &mut gpui::TestAppContext, right_dock_width: Option, right_sidebar_width: Option, ) { init_test(cx, |_| {}); let editor_width = px(700.); let window_width = editor_width + right_dock_width.unwrap_or_default() + right_sidebar_width.unwrap_or_default(); let buffer = cx.update(|cx| MultiBuffer::build_simple("", cx)); let window = cx.add_window(|window, cx| { let editor = cx.new(|cx| build_editor(buffer, window, cx)); window.focus(&editor.focus_handle(cx), cx); EditorWithRightOccluders { editor, editor_width, right_dock_width, right_sidebar_width, } }); let editor = window .read_with(cx, |root, _| root.editor.clone()) .expect("test window should contain editor"); let mut cx = gpui::VisualTestContext::from_window(*window, cx); cx.simulate_resize(size(window_width, px(500.))); cx.run_until_parked(); let mut cx = EditorTestContext::for_editor_in(editor, &mut cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.update_editor(|editor, _, _| { editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never); }); cx.set_state("abcdefghijklmnopqrstuvwxyzabcdefghijklmnˇopqrstuvwxyzabcdef"); propose_edits(&provider, vec![(40..41, "REPLACEMENT")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx.editor(|editor, _, _| { assert!(!editor.edit_prediction_visible_in_cursor_popover(true)); assert!(matches!( editor .active_edit_prediction .as_ref() .map(|state| &state.completion), Some(EditPrediction::Edit { display_mode: crate::EditDisplayMode::DiffPopover, .. }) )); }); cx.cx.update(|window, cx| { window.refresh(); let _ = window.draw(cx); }); cx.editor(|editor, _, _| { assert!( editor.last_position_map.is_some(), "editor should have rendered a position map" ); }); let popover_bounds = cx .cx .debug_bounds("edit_prediction_diff_popover") .expect("diff popover should render"); for selector in ["right_dock", "right_sidebar"] { if let Some(occluder_bounds) = cx.cx.debug_bounds(selector) { assert!( !popover_bounds.intersects(&occluder_bounds), "diff popover {popover_bounds:?} should not overlap {selector} {occluder_bounds:?}" ); } } } #[gpui::test] async fn test_edit_prediction_diff_popover_avoids_right_sidebar(cx: &mut gpui::TestAppContext) { assert_edit_prediction_diff_popover_avoids_right_occluders(cx, None, Some(px(300.))).await; } #[gpui::test] async fn test_edit_prediction_diff_popover_avoids_right_dock(cx: &mut gpui::TestAppContext) { assert_edit_prediction_diff_popover_avoids_right_occluders(cx, Some(px(300.)), None).await; } #[gpui::test] async fn test_edit_prediction_diff_popover_avoids_right_dock_and_sidebar( cx: &mut gpui::TestAppContext, ) { assert_edit_prediction_diff_popover_avoids_right_occluders(cx, Some(px(300.)), Some(px(300.))) .await; } #[gpui::test] async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let absolute_zero_celsius = ˇ;"); propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); assert_eq!(edits[0].1.as_ref(), "-273.15"); }); accept_completion(&mut cx); cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;") } #[gpui::test] async fn test_edit_prediction_cursor_position_inside_insertion(cx: &mut gpui::TestAppContext) { init_test(cx, |_| { eprintln!(""); }); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Buffer: "fn foo() {}" - we'll insert text and position cursor inside the insertion cx.set_state("fn foo() ˇ{}"); // Insert "bar()" at offset 9, with cursor at offset 2 within the insertion (after "ba") // This tests the case where cursor is inside newly inserted text propose_edits_with_cursor_position_in_insertion( &provider, vec![(9..9, "bar()")], 9, // anchor at the insertion point 2, // offset 2 within "bar()" puts cursor after "ba" &mut cx, ); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); assert_eq!(edits[0].1.as_ref(), "bar()"); }); accept_completion(&mut cx); // Cursor should be inside the inserted text at "baˇr()" cx.assert_editor_state("fn foo() baˇr(){}"); } #[gpui::test] async fn test_edit_prediction_cursor_position_outside_edit(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Buffer: "let x = ;" with cursor before semicolon - we'll insert "42" and position cursor elsewhere cx.set_state("let x = ˇ;"); // Insert "42" at offset 8, but set cursor_position to offset 4 (the 'x') // This tests that cursor moves to the predicted position, not the end of the edit propose_edits_with_cursor_position( &provider, vec![(8..8, "42")], Some(4), // cursor at offset 4 (the 'x'), NOT at the edit location &mut cx, ); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); assert_eq!(edits[0].1.as_ref(), "42"); }); accept_completion(&mut cx); // Cursor should be at offset 4 (the 'x'), not at the end of the inserted "42" cx.assert_editor_state("let ˇx = 42;"); } #[gpui::test] async fn test_edit_prediction_cursor_position_fallback(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let x = ˇ;"); // Propose an edit without a cursor position - should fall back to end of edit propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); accept_completion(&mut cx); // Cursor should be at the end of the inserted text (default behavior) cx.assert_editor_state("let x = 42ˇ;") } #[gpui::test] async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let pi = ˇ\"foo\";"); propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); assert_eq!(edits[0].1.as_ref(), "3.14159"); }); accept_completion(&mut cx); cx.assert_editor_state("let pi = 3.14159ˇ;") } #[gpui::test] async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 2+ lines above the proposed edit cx.set_state(indoc! {" line 0 line ˇ1 line 2 line 3 line "}); propose_edits( &provider, vec![(Point::new(4, 3)..Point::new(4, 3), " 4")], &mut cx, ); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3)); }); // When accepting, cursor is moved to the proposed location accept_completion(&mut cx); cx.assert_editor_state(indoc! {" line 0 line 1 line 2 line 3 linˇe "}); // Cursor is 2+ lines below the proposed edit cx.set_state(indoc! {" line 0 line line 2 line 3 line ˇ4 "}); propose_edits( &provider, vec![(Point::new(1, 3)..Point::new(1, 3), " 1")], &mut cx, ); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3)); }); // When accepting, cursor is moved to the proposed location accept_completion(&mut cx); cx.assert_editor_state(indoc! {" line 0 linˇe line 2 line 3 line 4 "}); } #[gpui::test] async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 3+ lines above the proposed edit cx.set_state(indoc! {" line 0 line ˇ1 line 2 line 3 line 4 line "}); let edit_location = Point::new(5, 3); propose_edits( &provider, vec![(edit_location..edit_location, " 5")], &mut cx, ); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); // If we move *towards* the completion, it stays active cx.set_selections_state(indoc! {" line 0 line 1 line ˇ2 line 3 line 4 line "}); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); // If we move *away* from the completion, it is discarded cx.set_selections_state(indoc! {" line ˇ0 line 1 line 2 line 3 line 4 line "}); cx.editor(|editor, _, _| { assert!(editor.active_edit_prediction.is_none()); }); // Cursor is 3+ lines below the proposed edit cx.set_state(indoc! {" line line 1 line 2 line 3 line ˇ4 line 5 "}); let edit_location = Point::new(0, 3); propose_edits( &provider, vec![(edit_location..edit_location, " 0")], &mut cx, ); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); // If we move *towards* the completion, it stays active cx.set_selections_state(indoc! {" line line 1 line 2 line ˇ3 line 4 line 5 "}); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); // If we move *away* from the completion, it is discarded cx.set_selections_state(indoc! {" line line 1 line 2 line 3 line 4 line ˇ5 "}); cx.editor(|editor, _, _| { assert!(editor.active_edit_prediction.is_none()); }); } #[gpui::test] async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeNonZedEditPredictionDelegate::default()); assign_editor_completion_provider_non_zed(provider.clone(), &mut cx); // Cursor is 2+ lines above the proposed edit cx.set_state(indoc! {" line 0 line ˇ1 line 2 line 3 line "}); propose_edits_non_zed( &provider, vec![(Point::new(4, 3)..Point::new(4, 3), " 4")], &mut cx, ); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); // For non-Zed providers, there should be no move completion (jump functionality disabled) cx.editor(|editor, _, _| { if let Some(completion_state) = &editor.active_edit_prediction { // Should be an Edit prediction, not a Move prediction match &completion_state.completion { EditPrediction::Edit { .. } => { // This is expected for non-Zed providers } EditPrediction::MoveWithin { .. } | EditPrediction::MoveOutside { .. } => { panic!( "Non-Zed providers should not show Move predictions (jump functionality)" ); } } } }); } #[gpui::test] async fn test_edit_prediction_refresh_suppressed_while_following(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| { editor.refresh_edit_prediction( false, false, EditPredictionRequestTrigger::Other, window, cx, ); editor.update_visible_edit_prediction(window, cx); }); assert_eq!( provider.read_with(&cx.cx, |provider, _| { provider.refresh_count.load(atomic::Ordering::SeqCst) }), 1 ); cx.editor(|editor, _, _| { assert!(editor.active_edit_prediction.is_some()); }); cx.update_editor(|editor, window, cx| { editor.leader_id = Some(CollaboratorId::PeerId(PeerId::default())); editor.refresh_edit_prediction( false, false, EditPredictionRequestTrigger::Other, window, cx, ); }); assert_eq!( provider.read_with(&cx.cx, |provider, _| { provider.refresh_count.load(atomic::Ordering::SeqCst) }), 1 ); cx.editor(|editor, _, _| { assert!(editor.active_edit_prediction.is_none()); }); cx.update_editor(|editor, window, cx| { editor.leader_id = None; editor.refresh_edit_prediction( false, false, EditPredictionRequestTrigger::Other, window, cx, ); }); assert_eq!( provider.read_with(&cx.cx, |provider, _| { provider.refresh_count.load(atomic::Ordering::SeqCst) }), 2 ); } #[gpui::test] async fn test_edit_prediction_preview_cleanup_on_toggle_off(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); // Bind `ctrl-shift-a` to accept the provided edit prediction. The actual key // binding here doesn't matter, we simply need to confirm that holding the // binding's modifiers triggers the edit prediction preview. cx.update(|cx| cx.bind_keys([KeyBinding::new("ctrl-shift-a", AcceptEditPrediction, None)])); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| { editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider); editor.update_visible_edit_prediction(window, cx) }); cx.editor(|editor, _, _| { assert!(editor.has_active_edit_prediction()); }); // Simulate pressing the modifiers for `AcceptEditPrediction`, namely // `ctrl-shift`, so that we can confirm that the edit prediction preview is // activated. let modifiers = Modifiers::control_shift(); cx.simulate_modifiers_change(modifiers); cx.run_until_parked(); cx.editor(|editor, _, _| { assert!(editor.edit_prediction_preview_is_active()); }); // Disable showing edit predictions without issuing a new modifiers changed // event, to confirm that the edit prediction preview is still active. cx.update_editor(|editor, window, cx| { editor.set_show_edit_predictions(Some(false), window, cx); }); cx.editor(|editor, _, _| { assert!(!editor.has_active_edit_prediction()); assert!(editor.edit_prediction_preview_is_active()); }); // Now release the modifiers // Simulate releasing all modifiers, ensuring that even with edit prediction // disabled, the edit prediction preview is cleaned up. cx.simulate_modifiers_change(Modifiers::none()); cx.run_until_parked(); cx.editor(|editor, _, _| { assert!(!editor.edit_prediction_preview_is_active()); }); } #[gpui::test] async fn test_hidden_edit_prediction_does_not_open_snippet_menu_on_word_input( cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); let mut cx = hidden_edit_prediction_snippet_test_context(cx).await; cx.simulate_input("t"); cx.run_until_parked(); cx.update_editor(|editor, _, _| { assert!(editor.has_active_edit_prediction()); assert!(editor.context_menu.borrow().is_none()); }); } #[gpui::test] async fn test_hidden_edit_prediction_opens_snippet_menu_for_strong_prefix_match( cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); let mut cx = hidden_edit_prediction_snippet_test_context(cx).await; cx.simulate_input("t"); cx.run_until_parked(); cx.simulate_input("h"); cx.run_until_parked(); cx.update_editor(|editor, _, _| { let Some(CodeContextMenu::Completions(menu)) = &*editor.context_menu.borrow() else { panic!("expected completions menu"); }; let entries = menu.entries.borrow(); assert!( entries .iter() .any(|entry| { entry.as_match().is_some_and(|m| m.string == "Theta") }) ); }); } #[gpui::test] async fn test_edit_prediction_preview_activates_when_prediction_arrives_with_modifier_held( cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); load_default_keymap(cx); update_test_language_settings(cx, &|settings| { settings.edit_predictions.get_or_insert_default().mode = Some(EditPredictionsMode::Subtle); }); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let x = ˇ;"); cx.editor(|editor, _, _| { assert!(!editor.has_active_edit_prediction()); assert!(!editor.edit_prediction_preview_is_active()); }); let preview_modifiers = cx.update_editor(|editor, window, cx| { *editor .preview_edit_prediction_keystroke(window, cx) .unwrap() .modifiers() }); cx.simulate_modifiers_change(preview_modifiers); cx.run_until_parked(); cx.editor(|editor, _, _| { assert!(!editor.has_active_edit_prediction()); assert!(editor.edit_prediction_preview_is_active()); }); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| { editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider); editor.update_visible_edit_prediction(window, cx) }); cx.editor(|editor, _, _| { assert!(editor.has_active_edit_prediction()); assert!( editor.edit_prediction_preview_is_active(), "prediction preview should activate immediately when the prediction arrives while the preview modifier is still held", ); }); } #[gpui::test] async fn test_edit_prediction_preview_does_not_hide_code_actions_on_modifier_press( cx: &mut gpui::TestAppContext, ) { init_test(cx, |_| {}); update_test_language_settings(cx, &|settings| { settings.edit_predictions.get_or_insert_default().mode = Some(EditPredictionsMode::Subtle); }); cx.update(|cx| { cx.bind_keys([KeyBinding::new( "ctrl-enter", AcceptEditPrediction, Some("Editor && edit_prediction && !showing_completions"), )]); }); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)), ..Default::default() }, cx, ) .await; cx.set_state(indoc! {" fn main() { let valueˇ = 1; } "}); let provider = cx.new(|_| FakeEditPredictionDelegate::default()); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider( Some(provider.clone()), EditPredictionRequestTrigger::EditorCreated, window, cx, ); }); let snapshot = cx.buffer_snapshot(); let edit_position = snapshot.anchor_after(Point::new(1, 13)); cx.update(|_, cx| { provider.update(cx, |provider, _| { provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local { id: None, edits: vec![(edit_position..edit_position, " + 1".into())], cursor_position: None, edit_preview: None, })) }) }); cx.update_editor(|editor, window, cx| { editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider); editor.update_visible_edit_prediction(window, cx); }); cx.update_editor(|editor, _, _| { assert!(editor.has_active_edit_prediction()); assert!(editor.stale_edit_prediction_in_menu.is_none()); }); let mut code_action_requests = cx.set_request_handler::( move |_, _, _| async move { Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( lsp::CodeAction { title: "Inline value".to_string(), kind: Some(lsp::CodeActionKind::QUICKFIX), ..Default::default() }, )])) }, ); cx.update_editor(|editor, window, cx| { editor.toggle_code_actions( &crate::actions::ToggleCodeActions { deployed_from: None, quick_launch: false, }, window, cx, ); }); code_action_requests.next().await; cx.run_until_parked(); cx.condition(|editor, _| editor.context_menu_visible()) .await; cx.update_editor(|editor, _, _| { assert!(!editor.has_active_edit_prediction()); assert!(editor.stale_edit_prediction_in_menu.is_some()); assert!(editor.context_menu_visible()); assert!(matches!( editor.context_menu.borrow().as_ref(), Some(crate::code_context_menus::CodeContextMenu::CodeActions(_)) )); assert!(!editor.edit_prediction_preview_is_active()); }); cx.simulate_modifiers_change(Modifiers::control()); cx.run_until_parked(); cx.update_editor(|editor, _, _| { assert!( !editor.edit_prediction_preview_is_active(), "modifier-only press should not activate edit prediction preview while code actions are open" ); assert!( editor.context_menu_visible(), "modifier-only press should not hide the code actions menu" ); assert!(matches!( editor.context_menu.borrow().as_ref(), Some(crate::code_context_menus::CodeContextMenu::CodeActions(_)) )); }); } #[gpui::test] async fn test_edit_prediction_preview_supersedes_completions_menu(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); update_test_language_settings(cx, &|settings| { settings.edit_predictions.get_or_insert_default().mode = Some(EditPredictionsMode::Subtle); }); cx.update(|cx| { cx.bind_keys([KeyBinding::new( "ctrl-enter", AcceptEditPrediction, Some("Editor && edit_prediction && showing_completions"), )]); }); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); assign_editor_completion_menu_provider(&mut cx); cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| { editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider); editor.update_visible_edit_prediction(window, cx); }); cx.update_editor(|editor, window, cx| { editor.show_completions(&ShowCompletions, window, cx); }); cx.run_until_parked(); cx.editor(|editor, _, _| { assert!(editor.has_active_edit_prediction()); assert!(editor.context_menu_visible()); assert!(matches!( editor.context_menu.borrow().as_ref(), Some(crate::code_context_menus::CodeContextMenu::Completions(_)) )); assert!(!editor.edit_prediction_preview_is_active()); }); cx.simulate_modifiers_change(Modifiers::control()); cx.run_until_parked(); cx.editor(|editor, _, _| { assert!(editor.edit_prediction_preview_is_active()); assert!(!editor.context_menu_visible()); assert!(matches!( editor.context_menu.borrow().as_ref(), Some(crate::code_context_menus::CodeContextMenu::Completions(_)) )); }); } fn load_default_keymap(cx: &mut gpui::TestAppContext) { cx.update(|cx| { cx.bind_keys( settings::KeymapFile::load_asset_allow_partial_failure( settings::DEFAULT_KEYMAP_PATH, cx, ) .expect("failed to load default keymap"), ); }); } #[gpui::test] async fn test_inline_edit_prediction_keybind_selection_cases(cx: &mut gpui::TestAppContext) { enum InlineKeybindState { Normal, ShowingCompletions, InLeadingWhitespace, ShowingCompletionsAndLeadingWhitespace, } enum ExpectedKeystroke { DefaultAccept, DefaultPreview, Literal(&'static str), } struct InlineKeybindCase { name: &'static str, use_default_keymap: bool, mode: EditPredictionsMode, extra_bindings: Vec, state: InlineKeybindState, expected_accept_keystroke: ExpectedKeystroke, expected_preview_keystroke: ExpectedKeystroke, expected_displayed_keystroke: ExpectedKeystroke, } init_test(cx, |_| {}); load_default_keymap(cx); let mut default_cx = EditorTestContext::new(cx).await; let provider = default_cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut default_cx); default_cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut default_cx); default_cx .update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); let (default_accept_keystroke, default_preview_keystroke) = default_cx.update_editor(|editor, window, cx| { let keybind_display = editor.edit_prediction_keybind_display( EditPredictionKeybindSurface::Inline, window, cx, ); let accept_keystroke = keybind_display .accept_keystroke .as_ref() .expect("default inline edit prediction should have an accept binding") .clone(); let preview_keystroke = keybind_display .preview_keystroke .as_ref() .expect("default inline edit prediction should have a preview binding") .clone(); (accept_keystroke, preview_keystroke) }); let cases = [ InlineKeybindCase { name: "default setup prefers tab over alt-tab for accept", use_default_keymap: true, mode: EditPredictionsMode::Eager, extra_bindings: Vec::new(), state: InlineKeybindState::Normal, expected_accept_keystroke: ExpectedKeystroke::DefaultAccept, expected_preview_keystroke: ExpectedKeystroke::DefaultPreview, expected_displayed_keystroke: ExpectedKeystroke::DefaultAccept, }, InlineKeybindCase { name: "subtle mode displays preview binding inline", use_default_keymap: true, mode: EditPredictionsMode::Subtle, extra_bindings: Vec::new(), state: InlineKeybindState::Normal, expected_accept_keystroke: ExpectedKeystroke::DefaultPreview, expected_preview_keystroke: ExpectedKeystroke::DefaultPreview, expected_displayed_keystroke: ExpectedKeystroke::DefaultPreview, }, InlineKeybindCase { name: "removing default tab binding still displays tab", use_default_keymap: true, mode: EditPredictionsMode::Eager, extra_bindings: vec![KeyBinding::new( "tab", NoAction, Some("Editor && edit_prediction && edit_prediction_mode == eager"), )], state: InlineKeybindState::Normal, expected_accept_keystroke: ExpectedKeystroke::DefaultPreview, expected_preview_keystroke: ExpectedKeystroke::DefaultPreview, expected_displayed_keystroke: ExpectedKeystroke::DefaultPreview, }, InlineKeybindCase { name: "custom-only rebound accept key uses replacement key", use_default_keymap: true, mode: EditPredictionsMode::Eager, extra_bindings: vec![KeyBinding::new( "ctrl-enter", AcceptEditPrediction, Some("Editor && edit_prediction"), )], state: InlineKeybindState::Normal, expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), }, InlineKeybindCase { name: "showing completions restores conflict-context binding", use_default_keymap: true, mode: EditPredictionsMode::Eager, extra_bindings: vec![KeyBinding::new( "ctrl-enter", AcceptEditPrediction, Some("Editor && edit_prediction && showing_completions"), )], state: InlineKeybindState::ShowingCompletions, expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), }, InlineKeybindCase { name: "leading whitespace restores conflict-context binding", use_default_keymap: false, mode: EditPredictionsMode::Eager, extra_bindings: vec![KeyBinding::new( "ctrl-enter", AcceptEditPrediction, Some("Editor && edit_prediction && in_leading_whitespace"), )], state: InlineKeybindState::InLeadingWhitespace, expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), }, InlineKeybindCase { name: "showing completions and leading whitespace restore combined conflict binding", use_default_keymap: false, mode: EditPredictionsMode::Eager, extra_bindings: vec![KeyBinding::new( "ctrl-enter", AcceptEditPrediction, Some("Editor && edit_prediction && showing_completions && in_leading_whitespace"), )], state: InlineKeybindState::ShowingCompletionsAndLeadingWhitespace, expected_accept_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_preview_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), expected_displayed_keystroke: ExpectedKeystroke::Literal("ctrl-enter"), }, ]; for case in cases { init_test(cx, |_| {}); if case.use_default_keymap { load_default_keymap(cx); } update_test_language_settings(cx, &|settings| { settings.edit_predictions.get_or_insert_default().mode = Some(case.mode); }); if !case.extra_bindings.is_empty() { cx.update(|cx| cx.bind_keys(case.extra_bindings.clone())); } let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); match case.state { InlineKeybindState::Normal | InlineKeybindState::ShowingCompletions => { cx.set_state("let x = ˇ;"); } InlineKeybindState::InLeadingWhitespace | InlineKeybindState::ShowingCompletionsAndLeadingWhitespace => { cx.set_state(indoc! {" fn main() { ˇ } "}); } } propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); if matches!( case.state, InlineKeybindState::ShowingCompletions | InlineKeybindState::ShowingCompletionsAndLeadingWhitespace ) { assign_editor_completion_menu_provider(&mut cx); cx.update_editor(|editor, window, cx| { editor.show_completions(&ShowCompletions, window, cx); }); cx.run_until_parked(); } cx.update_editor(|editor, window, cx| { assert!( editor.has_active_edit_prediction(), "case '{}' should have an active edit prediction", case.name ); let keybind_display = editor.edit_prediction_keybind_display( EditPredictionKeybindSurface::Inline, window, cx, ); let accept_keystroke = keybind_display .accept_keystroke .as_ref() .unwrap_or_else(|| panic!("case '{}' should have an accept binding", case.name)); let preview_keystroke = keybind_display .preview_keystroke .as_ref() .unwrap_or_else(|| panic!("case '{}' should have a preview binding", case.name)); let displayed_keystroke = keybind_display .displayed_keystroke .as_ref() .unwrap_or_else(|| panic!("case '{}' should have a displayed binding", case.name)); let expected_accept_keystroke = match case.expected_accept_keystroke { ExpectedKeystroke::DefaultAccept => default_accept_keystroke.clone(), ExpectedKeystroke::DefaultPreview => default_preview_keystroke.clone(), ExpectedKeystroke::Literal(keystroke) => KeybindingKeystroke::from_keystroke( Keystroke::parse(keystroke).expect("expected test keystroke to parse"), ), }; let expected_preview_keystroke = match case.expected_preview_keystroke { ExpectedKeystroke::DefaultAccept => default_accept_keystroke.clone(), ExpectedKeystroke::DefaultPreview => default_preview_keystroke.clone(), ExpectedKeystroke::Literal(keystroke) => KeybindingKeystroke::from_keystroke( Keystroke::parse(keystroke).expect("expected test keystroke to parse"), ), }; let expected_displayed_keystroke = match case.expected_displayed_keystroke { ExpectedKeystroke::DefaultAccept => default_accept_keystroke.clone(), ExpectedKeystroke::DefaultPreview => default_preview_keystroke.clone(), ExpectedKeystroke::Literal(keystroke) => KeybindingKeystroke::from_keystroke( Keystroke::parse(keystroke).expect("expected test keystroke to parse"), ), }; assert_eq!( accept_keystroke, &expected_accept_keystroke, "case '{}' selected the wrong accept binding", case.name ); assert_eq!( preview_keystroke, &expected_preview_keystroke, "case '{}' selected the wrong preview binding", case.name ); assert_eq!( displayed_keystroke, &expected_displayed_keystroke, "case '{}' selected the wrong displayed binding", case.name ); if matches!(case.mode, EditPredictionsMode::Subtle) { assert!( editor.edit_prediction_requires_modifier(), "case '{}' should require a modifier", case.name ); } }); } } #[gpui::test] async fn test_tab_accepts_edit_prediction_over_completion(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); load_default_keymap(cx); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); assert_eq!(edits[0].1.as_ref(), "42"); }); cx.simulate_keystroke("tab"); cx.run_until_parked(); cx.assert_editor_state("let x = 42ˇ;"); } #[gpui::test] async fn test_cursor_popover_edit_prediction_keybind_cases(cx: &mut gpui::TestAppContext) { enum CursorPopoverPredictionKind { SingleLine, MultiLine, SingleLineWithPreview, MultiLineWithPreview, DeleteSingleNewline, StaleSingleLineAfterMultiLine, } struct CursorPopoverCase { name: &'static str, prediction_kind: CursorPopoverPredictionKind, expected_action: EditPredictionKeybindAction, } let cases = [ CursorPopoverCase { name: "single line prediction uses accept action", prediction_kind: CursorPopoverPredictionKind::SingleLine, expected_action: EditPredictionKeybindAction::Accept, }, CursorPopoverCase { name: "multi line prediction uses preview action", prediction_kind: CursorPopoverPredictionKind::MultiLine, expected_action: EditPredictionKeybindAction::Preview, }, CursorPopoverCase { name: "single line prediction with preview still uses accept action", prediction_kind: CursorPopoverPredictionKind::SingleLineWithPreview, expected_action: EditPredictionKeybindAction::Accept, }, CursorPopoverCase { name: "multi line prediction with preview uses preview action", prediction_kind: CursorPopoverPredictionKind::MultiLineWithPreview, expected_action: EditPredictionKeybindAction::Preview, }, CursorPopoverCase { name: "single line newline deletion uses accept action", prediction_kind: CursorPopoverPredictionKind::DeleteSingleNewline, expected_action: EditPredictionKeybindAction::Accept, }, CursorPopoverCase { name: "stale multi line prediction does not force preview action", prediction_kind: CursorPopoverPredictionKind::StaleSingleLineAfterMultiLine, expected_action: EditPredictionKeybindAction::Accept, }, ]; for case in cases { init_test(cx, |_| {}); load_default_keymap(cx); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); match case.prediction_kind { CursorPopoverPredictionKind::SingleLine => { cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| { editor.update_visible_edit_prediction(window, cx) }); } CursorPopoverPredictionKind::MultiLine => { cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42\n43")], &mut cx); cx.update_editor(|editor, window, cx| { editor.update_visible_edit_prediction(window, cx) }); } CursorPopoverPredictionKind::SingleLineWithPreview => { cx.set_state("let x = ˇ;"); propose_edits_with_preview(&provider, vec![(8..8, "42")], &mut cx).await; cx.update_editor(|editor, window, cx| { editor.update_visible_edit_prediction(window, cx) }); } CursorPopoverPredictionKind::MultiLineWithPreview => { cx.set_state("let x = ˇ;"); propose_edits_with_preview(&provider, vec![(8..8, "42\n43")], &mut cx).await; cx.update_editor(|editor, window, cx| { editor.update_visible_edit_prediction(window, cx) }); } CursorPopoverPredictionKind::DeleteSingleNewline => { cx.set_state(indoc! {" fn main() { let value = 1; ˇprintln!(\"done\"); } "}); propose_edits( &provider, vec![(Point::new(1, 18)..Point::new(2, 17), "")], &mut cx, ); cx.update_editor(|editor, window, cx| { editor.update_visible_edit_prediction(window, cx) }); } CursorPopoverPredictionKind::StaleSingleLineAfterMultiLine => { cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42\n43")], &mut cx); cx.update_editor(|editor, window, cx| { editor.update_visible_edit_prediction(window, cx) }); cx.update_editor(|editor, _window, cx| { assert!(editor.active_edit_prediction.is_some()); assert!(editor.stale_edit_prediction_in_menu.is_none()); editor.take_active_edit_prediction(true, cx); assert!(editor.active_edit_prediction.is_none()); assert!(editor.stale_edit_prediction_in_menu.is_some()); }); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| { editor.update_visible_edit_prediction(window, cx) }); } } cx.update_editor(|editor, window, cx| { assert!( editor.has_active_edit_prediction(), "case '{}' should have an active edit prediction", case.name ); let keybind_display = editor.edit_prediction_keybind_display( EditPredictionKeybindSurface::CursorPopoverExpanded, window, cx, ); let accept_keystroke = keybind_display .accept_keystroke .as_ref() .unwrap_or_else(|| panic!("case '{}' should have an accept binding", case.name)); let preview_keystroke = keybind_display .preview_keystroke .as_ref() .unwrap_or_else(|| panic!("case '{}' should have a preview binding", case.name)); assert_eq!( keybind_display.action, case.expected_action, "case '{}' selected the wrong cursor popover action", case.name ); assert_eq!( accept_keystroke.key(), "tab", "case '{}' selected the wrong accept binding", case.name ); assert!( preview_keystroke.modifiers().modified(), "case '{}' should use a modified preview binding", case.name ); if matches!( case.prediction_kind, CursorPopoverPredictionKind::StaleSingleLineAfterMultiLine ) { assert!( editor.stale_edit_prediction_in_menu.is_none(), "case '{}' should clear stale menu state", case.name ); } }); } } fn assert_editor_active_edit_completion( cx: &mut EditorTestContext, assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range, Arc)>), ) { cx.editor(|editor, _, cx| { let completion_state = editor .active_edit_prediction .as_ref() .expect("editor has no active completion"); if let EditPrediction::Edit { edits, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), edits); } else { panic!("expected edit completion"); } }) } fn assert_editor_active_move_completion( cx: &mut EditorTestContext, assert: impl FnOnce(MultiBufferSnapshot, Anchor), ) { cx.editor(|editor, _, cx| { let completion_state = editor .active_edit_prediction .as_ref() .expect("editor has no active completion"); if let EditPrediction::MoveWithin { target, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), *target); } else { panic!("expected move completion"); } }) } #[gpui::test] async fn test_cancel_clears_stale_edit_prediction_in_menu(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); load_default_keymap(cx); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx.update_editor(|editor, _window, _cx| { assert!(editor.active_edit_prediction.is_some()); assert!(editor.stale_edit_prediction_in_menu.is_none()); }); cx.simulate_keystroke("escape"); cx.run_until_parked(); cx.update_editor(|editor, _window, _cx| { assert!(editor.active_edit_prediction.is_none()); assert!(editor.stale_edit_prediction_in_menu.is_none()); }); } #[gpui::test] async fn test_discard_clears_delegate_completion(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); load_default_keymap(cx); let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let x = ˇ;"); propose_edits(&provider, vec![(8..8, "42")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx.update_editor(|editor, _window, _cx| { assert!(editor.active_edit_prediction.is_some()); }); // Dismiss the prediction — this must call discard() on the delegate, // which should clear self.completion. cx.simulate_keystroke("escape"); cx.run_until_parked(); cx.update_editor(|editor, _window, _cx| { assert!(editor.active_edit_prediction.is_none()); }); // update_visible_edit_prediction must NOT bring the prediction back, // because discard() cleared self.completion in the delegate. cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx.update_editor(|editor, _window, _cx| { assert!( editor.active_edit_prediction.is_none(), "prediction must not resurface after discard()" ); }); } fn accept_completion(cx: &mut EditorTestContext) { cx.update_editor(|editor, window, cx| { editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx) }) } fn propose_edits( provider: &Entity, edits: Vec<(Range, &str)>, cx: &mut EditorTestContext, ) { propose_edits_with_cursor_position(provider, edits, None, cx); } async fn propose_edits_with_preview( provider: &Entity, edits: Vec<(Range, &str)>, cx: &mut EditorTestContext, ) { let snapshot = cx.buffer_snapshot(); let edits = edits .into_iter() .map(|(range, text)| { let anchor_range = snapshot.anchor_after(range.start.clone())..snapshot.anchor_before(range.end); (anchor_range, Arc::::from(text)) }) .collect::>(); let preview_edits = edits .iter() .map(|(range, text)| (range.clone(), text.clone())) .collect::>(); let edit_preview = cx .buffer(|buffer: &Buffer, app| buffer.preview_edits(preview_edits, app)) .await; let provider_edits = edits.into_iter().collect(); cx.update(|_, cx| { provider.update(cx, |provider, _| { provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local { id: None, edits: provider_edits, cursor_position: None, edit_preview: Some(edit_preview), })) }) }); } fn propose_edits_with_cursor_position( provider: &Entity, edits: Vec<(Range, &str)>, cursor_offset: Option, cx: &mut EditorTestContext, ) { let snapshot = cx.buffer_snapshot(); let cursor_position = cursor_offset .map(|offset| PredictedCursorPosition::at_anchor(snapshot.anchor_after(offset))); let edits = edits.into_iter().map(|(range, text)| { let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); (range, text.into()) }); cx.update(|_, cx| { provider.update(cx, |provider, _| { provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local { id: None, edits: edits.collect(), cursor_position, edit_preview: None, })) }) }); } fn propose_edits_with_cursor_position_in_insertion( provider: &Entity, edits: Vec<(Range, &str)>, anchor_offset: usize, offset_within_insertion: usize, cx: &mut EditorTestContext, ) { let snapshot = cx.buffer_snapshot(); // Use anchor_before (left bias) so the anchor stays at the insertion point // rather than moving past the inserted text let cursor_position = Some(PredictedCursorPosition::new( snapshot.anchor_before(anchor_offset), offset_within_insertion, )); let edits = edits.into_iter().map(|(range, text)| { let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); (range, text.into()) }); cx.update(|_, cx| { provider.update(cx, |provider, _| { provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local { id: None, edits: edits.collect(), cursor_position, edit_preview: None, })) }) }); } async fn hidden_edit_prediction_snippet_test_context( cx: &mut gpui::TestAppContext, ) -> EditorTestContext { let mut cx = EditorTestContext::new(cx).await; let provider = cx.new(|_| FakeEditPredictionDelegate::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.update_editor(|editor, _, cx| { editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never); editor.project().unwrap().update(cx, |project, cx| { project.snippets().update(cx, |snippets, _cx| { let snippet = project::snippet_provider::Snippet { prefix: vec!["Theta".to_string(), "turnstile".to_string()], body: "⊢".to_string(), description: Some("unicode symbol".to_string()), name: "unicode snippets".to_string(), }; snippets.add_snippet_for_test( None, PathBuf::from("test_snippets.json"), vec![Arc::new(snippet)], ); }); }) }); cx.set_state("ˇ"); propose_edits(&provider, vec![(0..0, "x")], &mut cx); cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx } fn assign_editor_completion_provider( provider: Entity, cx: &mut EditorTestContext, ) { cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider( Some(provider), EditPredictionRequestTrigger::EditorCreated, window, cx, ); }) } fn assign_editor_completion_menu_provider(cx: &mut EditorTestContext) { cx.update_editor(|editor, _, _| { editor.set_completion_provider(Some(Rc::new(FakeCompletionMenuProvider))); }); } fn propose_edits_non_zed( provider: &Entity, edits: Vec<(Range, &str)>, cx: &mut EditorTestContext, ) { let snapshot = cx.buffer_snapshot(); let edits = edits.into_iter().map(|(range, text)| { let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); (range, text.into()) }); cx.update(|_, cx| { provider.update(cx, |provider, _| { provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local { id: None, edits: edits.collect(), cursor_position: None, edit_preview: None, })) }) }); } fn assign_editor_completion_provider_non_zed( provider: Entity, cx: &mut EditorTestContext, ) { cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider( Some(provider), EditPredictionRequestTrigger::EditorCreated, window, cx, ); }) } struct FakeCompletionMenuProvider; impl CompletionProvider for FakeCompletionMenuProvider { fn completions( &self, buffer: &Entity, _buffer_position: text::Anchor, _trigger: CompletionContext, _window: &mut Window, cx: &mut Context, ) -> Task>> { let replace_range = text::Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()); let completions = ["fake_completion", "fake_completion_2"] .into_iter() .map(|label| Completion { replace_range: replace_range.clone(), new_text: label.to_string(), label: CodeLabel::plain(label.to_string(), None), documentation: None, source: CompletionSource::Custom, icon_path: None, icon_color: None, match_start: None, snippet_deduplication_key: None, insert_text_mode: None, confirm: None, group: None, }) .collect(); Task::ready(Ok(vec![CompletionResponse { completions, display_options: Default::default(), is_incomplete: false, }])) } fn is_completion_trigger( &self, _buffer: &Entity, _position: language::Anchor, _text: &str, _trigger_in_words: bool, _cx: &mut Context, ) -> bool { false } fn filter_completions(&self) -> bool { false } } #[derive(Default, Clone)] pub struct FakeEditPredictionDelegate { pub completion: Option, pub refresh_count: Arc, } impl FakeEditPredictionDelegate { pub fn set_edit_prediction( &mut self, completion: Option, ) { self.completion = completion; } } impl EditPredictionDelegate for FakeEditPredictionDelegate { fn name() -> &'static str { "fake-completion-provider" } fn display_name() -> &'static str { "Fake Completion Provider" } fn show_predictions_in_menu() -> bool { true } fn supports_jump_to_edit() -> bool { true } fn icons(&self, _cx: &gpui::App) -> EditPredictionIconSet { EditPredictionIconSet::new(IconName::ZedPredict) } fn is_enabled( &self, _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &gpui::App, ) -> bool { true } fn is_refreshing(&self, _cx: &gpui::App) -> bool { false } fn refresh( &mut self, _buffer: gpui::Entity, _cursor_position: language::Anchor, _debounce: bool, _trigger: edit_prediction_types::EditPredictionRequestTrigger, _cx: &mut gpui::Context, ) { self.refresh_count.fetch_add(1, atomic::Ordering::SeqCst); } fn accept(&mut self, _cx: &mut gpui::Context) {} fn discard( &mut self, _reason: edit_prediction_types::EditPredictionDiscardReason, _cx: &mut gpui::Context, ) { self.completion.take(); } fn suggest<'a>( &mut self, _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &mut gpui::Context, ) -> Option { self.completion.clone() } } #[derive(Default, Clone)] pub struct FakeNonZedEditPredictionDelegate { pub completion: Option, } impl FakeNonZedEditPredictionDelegate { pub fn set_edit_prediction( &mut self, completion: Option, ) { self.completion = completion; } } impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate { fn name() -> &'static str { "fake-non-zed-provider" } fn display_name() -> &'static str { "Fake Non-Zed Provider" } fn show_predictions_in_menu() -> bool { false } fn supports_jump_to_edit() -> bool { false } fn icons(&self, _cx: &gpui::App) -> EditPredictionIconSet { EditPredictionIconSet::new(IconName::ZedPredict) } fn is_enabled( &self, _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &gpui::App, ) -> bool { true } fn is_refreshing(&self, _cx: &gpui::App) -> bool { false } fn refresh( &mut self, _buffer: gpui::Entity, _cursor_position: language::Anchor, _debounce: bool, _trigger: edit_prediction_types::EditPredictionRequestTrigger, _cx: &mut gpui::Context, ) { } fn accept(&mut self, _cx: &mut gpui::Context) {} fn discard( &mut self, _reason: edit_prediction_types::EditPredictionDiscardReason, _cx: &mut gpui::Context, ) { self.completion.take(); } fn suggest<'a>( &mut self, _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &mut gpui::Context, ) -> Option { self.completion.clone() } }