Fix ep preview closing menus (#54194)

Ben Kunkle created

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #52559

Release Notes:

- Fixed an issue where holding the modifiers used in
`editor::AcceptEditPrediction` would cause the code action menu to
dissapear

Change summary

crates/editor/src/edit_prediction_tests.rs | 199 ++++++++++++++++++++++-
crates/editor/src/editor.rs                |   9 +
2 files changed, 194 insertions(+), 14 deletions(-)

Detailed changes

crates/editor/src/edit_prediction_tests.rs πŸ”—

@@ -1,6 +1,7 @@
 use edit_prediction_types::{
     EditPredictionDelegate, EditPredictionIconSet, PredictedCursorPosition,
 };
+use futures::StreamExt;
 use gpui::{
     Entity, KeyBinding, KeybindingKeystroke, Keystroke, Modifiers, NoAction, Task, prelude::*,
 };
@@ -25,7 +26,7 @@ use crate::{
     EditPredictionKeybindAction, EditPredictionKeybindSurface, MenuEditPredictionsPolicy,
     ShowCompletions,
     editor_tests::{init_test, update_test_language_settings},
-    test::editor_test_context::EditorTestContext,
+    test::{editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext},
 };
 use rpc::proto::PeerId;
 use workspace::CollaboratorId;
@@ -537,6 +538,172 @@ async fn test_edit_prediction_preview_activates_when_prediction_arrives_with_mod
     });
 }
 
+#[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()), 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::<lsp::request::CodeActionRequest, _, _>(
+        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(
@@ -1286,21 +1453,25 @@ impl CompletionProvider for FakeCompletionMenuProvider {
         _window: &mut Window,
         cx: &mut Context<crate::Editor>,
     ) -> Task<anyhow::Result<Vec<CompletionResponse>>> {
-        let completion = Completion {
-            replace_range: text::Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()),
-            new_text: "fake_completion".to_string(),
-            label: CodeLabel::plain("fake_completion".to_string(), None),
-            documentation: None,
-            source: CompletionSource::Custom,
-            icon_path: None,
-            match_start: None,
-            snippet_deduplication_key: None,
-            insert_text_mode: None,
-            confirm: None,
-        };
+        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,
+                match_start: None,
+                snippet_deduplication_key: None,
+                insert_text_mode: None,
+                confirm: None,
+            })
+            .collect();
 
         Task::ready(Ok(vec![CompletionResponse {
-            completions: vec![completion],
+            completions,
             display_options: Default::default(),
             is_incomplete: false,
         }]))

crates/editor/src/editor.rs πŸ”—

@@ -3031,6 +3031,15 @@ impl Editor {
         window: &mut Window,
         cx: &mut App,
     ) -> bool {
+        let can_supersede_active_menu =
+            self.context_menu.borrow().as_ref().is_none_or(|menu| {
+                !menu.visible() || matches!(menu, CodeContextMenu::Completions(_))
+            });
+
+        if !can_supersede_active_menu {
+            return false;
+        }
+
         let key_context = self.key_context_internal(true, window, cx);
         let actions: [&dyn Action; 3] = [
             &AcceptEditPrediction,