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

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`.
This commit is contained in:
Michael Sloan 2025-06-03 14:33:52 -06:00 committed by GitHub
parent 522abe8e59
commit 8c46a4f594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 70 additions and 38 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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