From 7a6a95c2cdd439dc2222f5c0936274bc5aac3722 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Soares?=
<37777652+Dnreikronos@users.noreply.github.com>
Date: Tue, 21 Apr 2026 09:46:24 -0300
Subject: [PATCH] editor: Fix edit predictions polluting completions menu
(#50403)
Closes #49565
Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [ ] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
Screenshots
Before:
After:
Release Notes:
- Fixed edit predictions polluting the completions menu with unrelated
snippets (e.g. Unicode symbols) when
`show_in_completions_menu` is disabled
---------
Co-authored-by: Ben Kunkle
---
crates/editor/src/edit_prediction_tests.rs | 71 +++++++++++++++++-
crates/editor/src/editor.rs | 87 +++++++++++++++++++---
2 files changed, 148 insertions(+), 10 deletions(-)
diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/edit_prediction_tests.rs
index 472e06b11b37048a32c5cf11619cb84edb3a0a3b..8078c90fa597fc3afde1cdda71509ad5de8c05a4 100644
--- a/crates/editor/src/edit_prediction_tests.rs
+++ b/crates/editor/src/edit_prediction_tests.rs
@@ -12,6 +12,7 @@ use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use project::{Completion, CompletionResponse, CompletionSource};
use std::{
ops::Range,
+ path::PathBuf,
rc::Rc,
sync::{
Arc,
@@ -22,7 +23,7 @@ use text::{Point, ToOffset};
use ui::prelude::*;
use crate::{
- AcceptEditPrediction, CompletionContext, CompletionProvider, EditPrediction,
+ AcceptEditPrediction, CodeContextMenu, CompletionContext, CompletionProvider, EditPrediction,
EditPredictionKeybindAction, EditPredictionKeybindSurface, MenuEditPredictionsPolicy,
ShowCompletions,
editor_tests::{init_test, update_test_language_settings},
@@ -488,6 +489,43 @@ async fn test_edit_prediction_preview_cleanup_on_toggle_off(cx: &mut gpui::TestA
});
}
+#[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.string == "Theta"));
+ });
+}
+
#[gpui::test]
async fn test_edit_prediction_preview_activates_when_prediction_arrives_with_modifier_held(
cx: &mut gpui::TestAppContext,
@@ -1395,6 +1433,37 @@ fn propose_edits_with_cursor_position_in_insertion(
});
}
+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,
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index d41db01368430b9273686dc9eff2d6d925410451..8f207b2ff6f8fc9295c0b9301c1def133dfd3fcb 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -5837,7 +5837,7 @@ impl Editor {
_ => self.open_or_update_completions_menu(
None,
Some(text.to_owned()).filter(|x| !x.is_empty()),
- true,
+ trigger_in_words,
window,
cx,
),
@@ -6325,8 +6325,15 @@ impl Editor {
let provider_responses = if let Some(provider) = &provider
&& load_provider_completions
{
- let trigger_character =
- trigger.filter(|trigger| buffer.read(cx).completion_triggers().contains(trigger));
+ let trigger_character = trigger
+ .as_ref()
+ .filter(|trigger| {
+ buffer
+ .read(cx)
+ .completion_triggers()
+ .contains(trigger.as_str())
+ })
+ .cloned();
let completion_context = CompletionContext {
trigger_kind: match &trigger_character {
Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER,
@@ -6374,16 +6381,51 @@ impl Editor {
Task::ready(BTreeMap::default())
};
+ let snippet_char_classifier = buffer_snapshot
+ .char_classifier_at(buffer_position)
+ .scope_context(Some(CharScopeContext::Completion));
+
let snippets = if let Some(provider) = &provider
&& provider.show_snippets()
&& let Some(project) = self.project()
{
- let char_classifier = buffer_snapshot
- .char_classifier_at(buffer_position)
- .scope_context(Some(CharScopeContext::Completion));
- project.update(cx, |project, cx| {
- snippet_completions(project, &buffer, buffer_position, char_classifier, cx)
- })
+ let word_trigger = trigger.as_ref().is_some_and(|trigger| {
+ !trigger.is_empty()
+ && trigger
+ .chars()
+ .all(|character| snippet_char_classifier.is_word(character))
+ });
+ let requires_strong_snippet_match = !menu_is_open && !trigger_in_words && word_trigger;
+ let load_snippet_completions = !requires_strong_snippet_match
+ || query.as_ref().is_some_and(|query| {
+ let project = project.read(cx);
+ has_strong_snippet_prefix_match(
+ &project,
+ &buffer,
+ buffer_position,
+ &snippet_char_classifier,
+ query,
+ cx,
+ )
+ });
+
+ if load_snippet_completions {
+ project.update(cx, |project, cx| {
+ snippet_completions(
+ project,
+ &buffer,
+ buffer_position,
+ snippet_char_classifier,
+ cx,
+ )
+ })
+ } else {
+ Task::ready(Ok(CompletionResponse {
+ completions: Vec::new(),
+ display_options: Default::default(),
+ is_incomplete: false,
+ }))
+ }
} else {
Task::ready(Ok(CompletionResponse {
completions: Vec::new(),
@@ -27751,6 +27793,33 @@ impl CodeActionProvider for Entity {
}
}
+fn has_strong_snippet_prefix_match(
+ project: &Project,
+ buffer: &Entity,
+ buffer_anchor: text::Anchor,
+ classifier: &CharClassifier,
+ query: &str,
+ cx: &App,
+) -> bool {
+ if query.chars().take(2).count() < 2 {
+ return false;
+ }
+
+ let query = query.to_lowercase();
+ let is_word_char = |character| classifier.is_word(character);
+ let languages = buffer.read(cx).languages_at(buffer_anchor);
+ let snippet_store = project.snippets().read(cx);
+
+ languages.iter().any(|language| {
+ snippet_store
+ .snippets_for(Some(language.lsp_id()), cx)
+ .iter()
+ .flat_map(|snippet| snippet.prefix.iter())
+ .flat_map(|prefix| snippet_candidate_suffixes(prefix, &is_word_char))
+ .any(|candidate| candidate.to_lowercase().starts_with(&query))
+ })
+}
+
fn snippet_completions(
project: &Project,
buffer: &Entity,