Fix ep preview closing menus (#54194)

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
This commit is contained in:
Ben Kunkle authored and GitHub committed 2026-04-20 10:20:34 -04:00
1 parent fb7fb48fae
commit 4ce9092834
2 files changed
+194 -14

No files matched your search

+185 -14
View File
@@ -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,
}]))
+9
View File
@@ -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,