Detailed changes
@@ -477,7 +477,7 @@ impl TextThreadEditor {
editor.insert(&format!("/{name}"), window, cx);
if command.accepts_arguments() {
editor.insert(" ", window, cx);
- editor.show_completions(&ShowCompletions::default(), window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
}
});
});
@@ -213,15 +213,6 @@ pub struct ExpandExcerptsDown {
pub(super) lines: u32,
}
-/// Shows code completion suggestions at the cursor position.
-#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
-#[action(namespace = editor)]
-#[serde(deny_unknown_fields)]
-pub struct ShowCompletions {
- #[serde(default)]
- pub(super) trigger: Option<String>,
-}
-
/// Handles text input in the editor.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
@@ -736,6 +727,8 @@ actions!(
SelectToStartOfParagraph,
/// Extends selection up.
SelectUp,
+ /// Shows code completion suggestions at the cursor position.
+ ShowCompletions,
/// Shows the system character palette.
ShowCharacterPalette,
/// Shows edit prediction at cursor.
@@ -252,8 +252,17 @@ enum MarkdownCacheKey {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CompletionsMenuSource {
+ /// Show all completions (words, snippets, LSP)
Normal,
+ /// Show only snippets (not words or LSP)
+ ///
+ /// Used after typing a non-word character
+ SnippetsOnly,
+ /// Tab stops within a snippet that have a predefined finite set of choices
SnippetChoices,
+ /// Show only words (not snippets or LSP)
+ ///
+ /// Used when word completions are explicitly triggered
Words { ignore_threshold: bool },
}
@@ -3142,7 +3142,7 @@ impl Editor {
};
if continue_showing {
- self.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ self.open_or_update_completions_menu(None, None, false, window, cx);
} else {
self.hide_context_menu(window, cx);
}
@@ -4972,57 +4972,18 @@ impl Editor {
ignore_threshold: false,
}),
None,
- 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);
+ );
}
- }
- }
-
- fn is_completion_trigger(
- &self,
- text: &str,
- trigger_in_words: bool,
- menu_is_open: bool,
- cx: &mut Context<Self>,
- ) -> bool {
- let position = self.selections.newest_anchor().head();
- let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else {
- return false;
- };
-
- if let Some(completion_provider) = &self.completion_provider {
- completion_provider.is_completion_trigger(
- &buffer,
- position.text_anchor,
- text,
- trigger_in_words,
- menu_is_open,
+ _ => self.open_or_update_completions_menu(
+ None,
+ Some(text.to_owned()).filter(|x| !x.is_empty()),
+ true,
+ window,
cx,
- )
- } else {
- false
+ ),
}
}
@@ -5300,6 +5261,7 @@ impl Editor {
ignore_threshold: true,
}),
None,
+ false,
window,
cx,
);
@@ -5307,17 +5269,18 @@ impl Editor {
pub fn show_completions(
&mut self,
- options: &ShowCompletions,
+ _: &ShowCompletions,
window: &mut Window,
cx: &mut Context<Self>,
) {
- self.open_or_update_completions_menu(None, options.trigger.as_deref(), window, cx);
+ self.open_or_update_completions_menu(None, None, false, window, cx);
}
fn open_or_update_completions_menu(
&mut self,
requested_source: Option<CompletionsMenuSource>,
- trigger: Option<&str>,
+ trigger: Option<String>,
+ trigger_in_words: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -5325,6 +5288,15 @@ impl Editor {
return;
}
+ let completions_source = self
+ .context_menu
+ .borrow()
+ .as_ref()
+ .and_then(|menu| match menu {
+ CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source),
+ CodeContextMenu::CodeActions(_) => None,
+ });
+
let multibuffer_snapshot = self.buffer.read(cx).read(cx);
// Typically `start` == `end`, but with snippet tabstop choices the default choice is
@@ -5372,7 +5344,8 @@ impl Editor {
ignore_word_threshold = ignore_threshold;
None
}
- Some(CompletionsMenuSource::SnippetChoices) => {
+ Some(CompletionsMenuSource::SnippetChoices)
+ | Some(CompletionsMenuSource::SnippetsOnly) => {
log::error!("bug: SnippetChoices requested_source is not handled");
None
}
@@ -5386,13 +5359,19 @@ impl Editor {
.as_ref()
.is_none_or(|provider| provider.filter_completions());
+ let was_snippets_only = matches!(
+ completions_source,
+ Some(CompletionsMenuSource::SnippetsOnly)
+ );
+
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
if filter_completions {
menu.filter(query.clone(), provider.clone(), window, cx);
}
// When `is_incomplete` is false, no need to re-query completions when the current query
// is a suffix of the initial query.
- if !menu.is_incomplete {
+ let was_complete = !menu.is_incomplete;
+ if was_complete && !was_snippets_only {
// If the new query is a suffix of the old query (typing more characters) and
// the previous result was complete, the existing completions can be filtered.
//
@@ -5416,23 +5395,6 @@ impl Editor {
}
};
- let trigger_kind = match trigger {
- Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
- CompletionTriggerKind::TRIGGER_CHARACTER
- }
- _ => CompletionTriggerKind::INVOKED,
- };
- let completion_context = CompletionContext {
- trigger_character: trigger.and_then(|trigger| {
- if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
- Some(String::from(trigger))
- } else {
- None
- }
- }),
- trigger_kind,
- };
-
let Anchor {
excerpt_id: buffer_excerpt_id,
text_anchor: buffer_position,
@@ -5490,49 +5452,72 @@ impl Editor {
&& match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
- });
+ })
+ || (provider.is_some() && completion_settings.words == WordsCompletionMode::Disabled);
+
+ let mut words = if omit_word_completions {
+ Task::ready(BTreeMap::default())
+ } else {
+ cx.background_spawn(async move {
+ buffer_snapshot.words_in_range(WordsQuery {
+ fuzzy_contents: None,
+ range: word_search_range,
+ skip_digits,
+ })
+ })
+ };
- let (mut words, provider_responses) = match &provider {
- Some(provider) => {
- let provider_responses = provider.completions(
- buffer_excerpt_id,
+ let load_provider_completions = provider.as_ref().is_some_and(|provider| {
+ trigger.as_ref().is_none_or(|trigger| {
+ provider.is_completion_trigger(
&buffer,
- buffer_position,
- completion_context,
- window,
+ position.text_anchor,
+ trigger,
+ trigger_in_words,
+ completions_source.is_some(),
cx,
- );
+ )
+ })
+ });
- let words = match (omit_word_completions, completion_settings.words) {
- (true, _) | (_, WordsCompletionMode::Disabled) => {
- Task::ready(BTreeMap::default())
- }
- (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx
- .background_spawn(async move {
- buffer_snapshot.words_in_range(WordsQuery {
- fuzzy_contents: None,
- range: word_search_range,
- skip_digits,
- })
- }),
- };
+ 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 completion_context = CompletionContext {
+ trigger_kind: match &trigger_character {
+ Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER,
+ None => CompletionTriggerKind::INVOKED,
+ },
+ trigger_character,
+ };
- (words, provider_responses)
- }
- None => {
- let words = if omit_word_completions {
- Task::ready(BTreeMap::default())
- } else {
- cx.background_spawn(async move {
- buffer_snapshot.words_in_range(WordsQuery {
- fuzzy_contents: None,
- range: word_search_range,
- skip_digits,
- })
- })
- };
- (words, Task::ready(Ok(Vec::new())))
- }
+ provider.completions(
+ buffer_excerpt_id,
+ &buffer,
+ buffer_position,
+ completion_context,
+ window,
+ cx,
+ )
+ } else {
+ Task::ready(Ok(Vec::new()))
+ };
+
+ let snippets = if let Some(provider) = &provider
+ && provider.show_snippets()
+ && let Some(project) = self.project()
+ {
+ project.update(cx, |project, cx| {
+ snippet_completions(project, &buffer, buffer_position, cx)
+ })
+ } else {
+ Task::ready(Ok(CompletionResponse {
+ completions: Vec::new(),
+ display_options: Default::default(),
+ is_incomplete: false,
+ }))
};
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
@@ -5590,6 +5575,13 @@ impl Editor {
confirm: None,
}));
+ completions.extend(
+ snippets
+ .await
+ .into_iter()
+ .flat_map(|response| response.completions),
+ );
+
let menu = if completions.is_empty() {
None
} else {
@@ -5601,7 +5593,11 @@ impl Editor {
.map(|workspace| workspace.read(cx).app_state().languages.clone());
let menu = CompletionsMenu::new(
id,
- requested_source.unwrap_or(CompletionsMenuSource::Normal),
+ requested_source.unwrap_or(if load_provider_completions {
+ CompletionsMenuSource::Normal
+ } else {
+ CompletionsMenuSource::SnippetsOnly
+ }),
sort_completions,
show_completion_documentation,
position,
@@ -5931,7 +5927,7 @@ impl Editor {
.as_ref()
.is_some_and(|confirm| confirm(intent, window, cx));
if show_new_completions_on_confirm {
- self.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ self.open_or_update_completions_menu(None, None, false, window, cx);
}
let provider = self.completion_provider.as_ref()?;
@@ -12724,6 +12720,10 @@ impl Editor {
});
}
+ // 🤔 | .. | show_in_menu |
+ // | .. | true true
+ // | had_edit_prediction | false true
+
let trigger_in_words =
this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
@@ -22925,6 +22925,10 @@ pub trait CompletionProvider {
fn filter_completions(&self) -> bool {
true
}
+
+ fn show_snippets(&self) -> bool {
+ false
+ }
}
pub trait CodeActionProvider {
@@ -23185,16 +23189,8 @@ impl CompletionProvider for Entity<Project> {
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
self.update(cx, |project, cx| {
- let snippets = snippet_completions(project, buffer, buffer_position, cx);
- let project_completions = project.completions(buffer, buffer_position, options, cx);
- cx.background_spawn(async move {
- let mut responses = project_completions.await?;
- let snippets = snippets.await?;
- if !snippets.completions.is_empty() {
- responses.push(snippets);
- }
- Ok(responses)
- })
+ let task = project.completions(buffer, buffer_position, options, cx);
+ cx.background_spawn(task)
})
}
@@ -23266,6 +23262,10 @@ impl CompletionProvider for Entity<Project> {
buffer.completion_triggers().contains(text)
}
+
+ fn show_snippets(&self) -> bool {
+ true
+ }
}
impl SemanticsProvider for Entity<Project> {
@@ -13883,7 +13883,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
cx.set_state(&run.initial_state);
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
let counter = Arc::new(AtomicUsize::new(0));
@@ -13943,7 +13943,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
let counter = Arc::new(AtomicUsize::new(0));
@@ -13979,7 +13979,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14066,7 +14066,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
"};
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14120,7 +14120,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
"};
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14169,7 +14169,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
"};
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
handle_completion_request_with_insert_and_replace(
&mut cx,
@@ -14320,7 +14320,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
});
editor.update_in(cx, |editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
fake_server
@@ -14559,7 +14559,7 @@ async fn test_completion(cx: &mut TestAppContext) {
cx.assert_editor_state("editor.cloˇ");
assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
handle_completion_request(
"editor.<clo|>",
@@ -14958,7 +14958,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
4.5f32
"});
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions::default(), window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
@@ -14984,7 +14984,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
33.35f32
"});
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions::default(), window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
@@ -15408,13 +15408,7 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
cx.set_state("fn a() {}\n nˇ");
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
- editor.show_completions(
- &ShowCompletions {
- trigger: Some("\n".into()),
- },
- window,
- cx,
- );
+ editor.trigger_completion_on_input("n", true, window, cx)
});
cx.executor().run_until_parked();
@@ -15512,7 +15506,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
})))
});
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -15561,7 +15555,7 @@ int fn_branch(bool do_branch1, bool do_branch2);
})))
});
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -18051,7 +18045,7 @@ async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
}
});
cx.update_editor(|editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
completion_requests.next().await;
cx.condition(|editor, _| editor.context_menu_visible())
@@ -24447,7 +24441,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
])))
});
editor.update_in(cx, |editor, window, cx| {
- editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ editor.show_completions(&ShowCompletions, window, cx);
});
cx.run_until_parked();
completion_handle.next().await.unwrap();