Make completions menu stay open after after it's manually requested (#32015)

Michael Sloan created

Also includes a clarity refactoring to remove
`ignore_completion_provider`.

Closes #15549

Release Notes:

- Fixed completions menu closing on typing after being requested while
`show_completions_on_input: false`.

Change summary

crates/agent/src/context_picker/completion_provider.rs |  5 
crates/assistant_context_editor/src/slash_command.rs   |  1 
crates/collab_ui/src/chat_panel/message_editor.rs      |  1 
crates/debugger_ui/src/session/running/console.rs      |  1 
crates/editor/src/code_context_menus.rs                | 15 +
crates/editor/src/editor.rs                            | 82 +++++++----
crates/inspector_ui/src/div_inspector.rs               |  5 
7 files changed, 71 insertions(+), 39 deletions(-)

Detailed changes

crates/agent/src/context_picker/completion_provider.rs 🔗

@@ -926,8 +926,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         &self,
         buffer: &Entity<language::Buffer>,
         position: language::Anchor,
-        _: &str,
-        _: bool,
+        _text: &str,
+        _trigger_in_words: bool,
+        _menu_is_open: bool,
         cx: &mut Context<Editor>,
     ) -> bool {
         let buffer = buffer.read(cx);

crates/assistant_context_editor/src/slash_command.rs 🔗

@@ -342,6 +342,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
         position: language::Anchor,
         _text: &str,
         _trigger_in_words: bool,
+        _menu_is_open: bool,
         cx: &mut Context<Editor>,
     ) -> bool {
         let buffer = buffer.read(cx);

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -89,6 +89,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
         _position: language::Anchor,
         text: &str,
         _trigger_in_words: bool,
+        _menu_is_open: bool,
         _cx: &mut Context<Editor>,
     ) -> bool {
         text == "@"

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -309,6 +309,7 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
         _position: language::Anchor,
         _text: &str,
         _trigger_in_words: bool,
+        _menu_is_open: bool,
         _cx: &mut Context<Editor>,
     ) -> bool {
         true

crates/editor/src/code_context_menus.rs 🔗

@@ -194,6 +194,7 @@ pub enum ContextMenuOrigin {
 
 pub struct CompletionsMenu {
     pub id: CompletionId,
+    pub source: CompletionsMenuSource,
     sort_completions: bool,
     pub initial_position: Anchor,
     pub initial_query: Option<Arc<String>>,
@@ -208,7 +209,6 @@ pub struct CompletionsMenu {
     scroll_handle: UniformListScrollHandle,
     resolve_completions: bool,
     show_completion_documentation: bool,
-    pub(super) ignore_completion_provider: bool,
     last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
     markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
     language_registry: Option<Arc<LanguageRegistry>>,
@@ -227,6 +227,13 @@ enum MarkdownCacheKey {
     },
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum CompletionsMenuSource {
+    Normal,
+    SnippetChoices,
+    Words,
+}
+
 // TODO: There should really be a wrapper around fuzzy match tasks that does this.
 impl Drop for CompletionsMenu {
     fn drop(&mut self) {
@@ -237,9 +244,9 @@ impl Drop for CompletionsMenu {
 impl CompletionsMenu {
     pub fn new(
         id: CompletionId,
+        source: CompletionsMenuSource,
         sort_completions: bool,
         show_completion_documentation: bool,
-        ignore_completion_provider: bool,
         initial_position: Anchor,
         initial_query: Option<Arc<String>>,
         is_incomplete: bool,
@@ -258,13 +265,13 @@ impl CompletionsMenu {
 
         let completions_menu = Self {
             id,
+            source,
             sort_completions,
             initial_position,
             initial_query,
             is_incomplete,
             buffer,
             show_completion_documentation,
-            ignore_completion_provider,
             completions: RefCell::new(completions).into(),
             match_candidates,
             entries: Rc::new(RefCell::new(Box::new([]))),
@@ -328,6 +335,7 @@ impl CompletionsMenu {
             .collect();
         Self {
             id,
+            source: CompletionsMenuSource::SnippetChoices,
             sort_completions,
             initial_position: selection.start,
             initial_query: None,
@@ -342,7 +350,6 @@ impl CompletionsMenu {
             scroll_handle: UniformListScrollHandle::new(),
             resolve_completions: false,
             show_completion_documentation: false,
-            ignore_completion_provider: false,
             last_rendered_range: RefCell::new(None).into(),
             markdown_cache: RefCell::new(VecDeque::new()).into(),
             language_registry: None,

crates/editor/src/editor.rs 🔗

@@ -211,8 +211,11 @@ use workspace::{
     searchable::SearchEvent,
 };
 
-use crate::hover_links::{find_url, find_url_from_range};
 use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
+use crate::{
+    code_context_menus::CompletionsMenuSource,
+    hover_links::{find_url, find_url_from_range},
+};
 
 pub const FILE_HEADER_HEIGHT: u32 = 2;
 pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
@@ -4510,30 +4513,40 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let ignore_completion_provider = self
+        let completions_source = self
             .context_menu
             .borrow()
             .as_ref()
-            .map(|menu| match menu {
-                CodeContextMenu::Completions(completions_menu) => {
-                    completions_menu.ignore_completion_provider
-                }
-                CodeContextMenu::CodeActions(_) => false,
-            })
-            .unwrap_or(false);
-
-        if ignore_completion_provider {
-            self.show_word_completions(&ShowWordCompletions, window, cx);
-        } else if self.is_completion_trigger(text, trigger_in_words, cx) {
-            self.show_completions(
-                &ShowCompletions {
-                    trigger: Some(text.to_owned()).filter(|x| !x.is_empty()),
-                },
-                window,
-                cx,
-            );
-        } else {
-            self.hide_context_menu(window, cx);
+            .and_then(|menu| match menu {
+                CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source),
+                CodeContextMenu::CodeActions(_) => None,
+            });
+
+        match completions_source {
+            Some(CompletionsMenuSource::Words) => {
+                self.show_word_completions(&ShowWordCompletions, window, cx)
+            }
+            Some(CompletionsMenuSource::Normal)
+            | Some(CompletionsMenuSource::SnippetChoices)
+            | None
+                if self.is_completion_trigger(
+                    text,
+                    trigger_in_words,
+                    completions_source.is_some(),
+                    cx,
+                ) =>
+            {
+                self.show_completions(
+                    &ShowCompletions {
+                        trigger: Some(text.to_owned()).filter(|x| !x.is_empty()),
+                    },
+                    window,
+                    cx,
+                )
+            }
+            _ => {
+                self.hide_context_menu(window, cx);
+            }
         }
     }
 
@@ -4541,6 +4554,7 @@ impl Editor {
         &self,
         text: &str,
         trigger_in_words: bool,
+        menu_is_open: bool,
         cx: &mut Context<Self>,
     ) -> bool {
         let position = self.selections.newest_anchor().head();
@@ -4558,6 +4572,7 @@ impl Editor {
                 position.text_anchor,
                 text,
                 trigger_in_words,
+                menu_is_open,
                 cx,
             )
         } else {
@@ -5008,7 +5023,7 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.open_or_update_completions_menu(true, None, window, cx);
+        self.open_or_update_completions_menu(Some(CompletionsMenuSource::Words), None, window, cx);
     }
 
     pub fn show_completions(
@@ -5017,12 +5032,12 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.open_or_update_completions_menu(false, options.trigger.as_deref(), window, cx);
+        self.open_or_update_completions_menu(None, options.trigger.as_deref(), window, cx);
     }
 
     fn open_or_update_completions_menu(
         &mut self,
-        ignore_completion_provider: bool,
+        requested_source: Option<CompletionsMenuSource>,
         trigger: Option<&str>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -5047,10 +5062,13 @@ impl Editor {
             Self::completion_query(&self.buffer.read(cx).read(cx), position)
                 .map(|query| query.into());
 
-        let provider = if ignore_completion_provider {
-            None
-        } else {
-            self.completion_provider.clone()
+        let provider = match requested_source {
+            Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(),
+            Some(CompletionsMenuSource::Words) => None,
+            Some(CompletionsMenuSource::SnippetChoices) => {
+                log::error!("bug: SnippetChoices requested_source is not handled");
+                None
+            }
         };
 
         let sort_completions = provider
@@ -5246,9 +5264,9 @@ impl Editor {
                         .map(|workspace| workspace.read(cx).app_state().languages.clone());
                     let menu = CompletionsMenu::new(
                         id,
+                        requested_source.unwrap_or(CompletionsMenuSource::Normal),
                         sort_completions,
                         show_completion_documentation,
-                        ignore_completion_provider,
                         position,
                         query.clone(),
                         is_incomplete,
@@ -20295,6 +20313,7 @@ pub trait CompletionProvider {
         position: language::Anchor,
         text: &str,
         trigger_in_words: bool,
+        menu_is_open: bool,
         cx: &mut Context<Editor>,
     ) -> bool;
 
@@ -20612,6 +20631,7 @@ impl CompletionProvider for Entity<Project> {
         position: language::Anchor,
         text: &str,
         trigger_in_words: bool,
+        menu_is_open: bool,
         cx: &mut Context<Editor>,
     ) -> bool {
         let mut chars = text.chars();
@@ -20626,7 +20646,7 @@ impl CompletionProvider for Entity<Project> {
 
         let buffer = buffer.read(cx);
         let snapshot = buffer.snapshot();
-        if !snapshot.settings_at(position, cx).show_completions_on_input {
+        if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
             return false;
         }
         let classifier = snapshot.char_classifier_at(position).for_completion(true);

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -685,8 +685,9 @@ impl CompletionProvider for RustStyleCompletionProvider {
         &self,
         buffer: &Entity<language::Buffer>,
         position: language::Anchor,
-        _: &str,
-        _: bool,
+        _text: &str,
+        _trigger_in_words: bool,
+        _menu_is_open: bool,
         cx: &mut Context<Editor>,
     ) -> bool {
         completion_replace_range(&buffer.read(cx).snapshot(), &position).is_some()