diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 991bd52efb1b0d30dc25040587dfde040dd1f58f..44c80a2258d1146fb7a5f2fb6124d08d61d8cb57 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -477,7 +477,7 @@ impl TextThreadEditor { editor.insert(&format!("/{name}"), window, cx); if command.accepts_arguments() { editor.insert(" ", window, cx); - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions::default(), window, cx); } }); }); diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 1c5e5a29c7c2e6ae2e7a9bb33048550770f71d1e..38ae42c3814fa09e50a92dcc20f0a34bad82ea40 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -213,6 +213,15 @@ 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, +} + /// Handles text input in the editor. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] @@ -723,8 +732,6 @@ 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. diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 9e29cd955a80c7025ef2ff1ee5aaf38c665bed1a..b7f3d57870a9504b7e6f9f736a0951b9b4b733e5 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -252,17 +252,8 @@ 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 }, } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8a62f086af502be34b6a9b6baa91ce4e7be576de..fdd5b01744a7a2b89edfb61bfc379af203fd7058 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3138,7 +3138,7 @@ impl Editor { }; if continue_showing { - self.open_or_update_completions_menu(None, None, false, window, cx); + self.show_completions(&ShowCompletions { trigger: None }, window, cx); } else { self.hide_context_menu(window, cx); } @@ -4968,18 +4968,57 @@ impl Editor { ignore_threshold: false, }), None, - trigger_in_words, window, cx, ); } - _ => self.open_or_update_completions_menu( - None, - Some(text.to_owned()).filter(|x| !x.is_empty()), - true, - window, + 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, + ) -> 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, cx, - ), + ) + } else { + false } } @@ -5257,7 +5296,6 @@ impl Editor { ignore_threshold: true, }), None, - false, window, cx, ); @@ -5265,18 +5303,17 @@ impl Editor { pub fn show_completions( &mut self, - _: &ShowCompletions, + options: &ShowCompletions, window: &mut Window, cx: &mut Context, ) { - self.open_or_update_completions_menu(None, None, false, window, cx); + self.open_or_update_completions_menu(None, options.trigger.as_deref(), window, cx); } fn open_or_update_completions_menu( &mut self, requested_source: Option, - trigger: Option, - trigger_in_words: bool, + trigger: Option<&str>, window: &mut Window, cx: &mut Context, ) { @@ -5284,15 +5321,6 @@ 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 @@ -5340,8 +5368,7 @@ impl Editor { ignore_word_threshold = ignore_threshold; None } - Some(CompletionsMenuSource::SnippetChoices) - | Some(CompletionsMenuSource::SnippetsOnly) => { + Some(CompletionsMenuSource::SnippetChoices) => { log::error!("bug: SnippetChoices requested_source is not handled"); None } @@ -5355,19 +5382,13 @@ 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. - let was_complete = !menu.is_incomplete; - if was_complete && !was_snippets_only { + if !menu.is_incomplete { // 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. // @@ -5391,6 +5412,23 @@ 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, @@ -5448,72 +5486,49 @@ 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 load_provider_completions = provider.as_ref().is_some_and(|provider| { - trigger.as_ref().is_none_or(|trigger| { - provider.is_completion_trigger( + let (mut words, provider_responses) = match &provider { + Some(provider) => { + let provider_responses = provider.completions( + buffer_excerpt_id, &buffer, - position.text_anchor, - trigger, - trigger_in_words, - completions_source.is_some(), + buffer_position, + completion_context, + window, cx, - ) - }) - }); - - 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, - }; + ); - provider.completions( - buffer_excerpt_id, - &buffer, - buffer_position, - completion_context, - window, - cx, - ) - } else { - Task::ready(Ok(Vec::new())) - }; + 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 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, - })) + (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()))) + } }; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -5571,13 +5586,6 @@ impl Editor { confirm: None, })); - completions.extend( - snippets - .await - .into_iter() - .flat_map(|response| response.completions), - ); - let menu = if completions.is_empty() { None } else { @@ -5589,11 +5597,7 @@ impl Editor { .map(|workspace| workspace.read(cx).app_state().languages.clone()); let menu = CompletionsMenu::new( id, - requested_source.unwrap_or(if load_provider_completions { - CompletionsMenuSource::Normal - } else { - CompletionsMenuSource::SnippetsOnly - }), + requested_source.unwrap_or(CompletionsMenuSource::Normal), sort_completions, show_completion_documentation, position, @@ -5923,7 +5927,7 @@ impl Editor { .as_ref() .is_some_and(|confirm| confirm(intent, window, cx)); if show_new_completions_on_confirm { - self.open_or_update_completions_menu(None, None, false, window, cx); + self.show_completions(&ShowCompletions { trigger: None }, window, cx); } let provider = self.completion_provider.as_ref()?; @@ -12683,10 +12687,6 @@ 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; @@ -22888,10 +22888,6 @@ pub trait CompletionProvider { fn filter_completions(&self) -> bool { true } - - fn show_snippets(&self) -> bool { - false - } } pub trait CodeActionProvider { @@ -23152,8 +23148,16 @@ impl CompletionProvider for Entity { cx: &mut Context, ) -> Task>> { self.update(cx, |project, cx| { - let task = project.completions(buffer, buffer_position, options, cx); - cx.background_spawn(task) + 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) + }) }) } @@ -23225,10 +23229,6 @@ impl CompletionProvider for Entity { buffer.completion_triggers().contains(text) } - - fn show_snippets(&self) -> bool { - true - } } impl SemanticsProvider for Entity { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 024a19d44095a6a5927e0ba4f5d661da593cb0bb..2428032a4aae54d746cad50ac676e449cd79d9ce 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -13760,7 +13760,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, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); let counter = Arc::new(AtomicUsize::new(0)); @@ -13820,7 +13820,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, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); let counter = Arc::new(AtomicUsize::new(0)); @@ -13856,7 +13856,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, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); handle_completion_request_with_insert_and_replace( &mut cx, @@ -13943,7 +13943,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, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); handle_completion_request_with_insert_and_replace( &mut cx, @@ -13997,7 +13997,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, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); handle_completion_request_with_insert_and_replace( &mut cx, @@ -14046,7 +14046,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, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); handle_completion_request_with_insert_and_replace( &mut cx, @@ -14197,7 +14197,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte }); editor.update_in(cx, |editor, window, cx| { - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); fake_server @@ -14436,7 +14436,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, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); handle_completion_request( "editor.", @@ -14835,7 +14835,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { 4.5f32 "}); cx.update_editor(|editor, window, cx| { - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions::default(), window, cx); }); cx.executor().run_until_parked(); cx.condition(|editor, _| editor.context_menu_visible()) @@ -14861,7 +14861,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { 33.35f32 "}); cx.update_editor(|editor, window, cx| { - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions::default(), window, cx); }); cx.executor().run_until_parked(); cx.condition(|editor, _| editor.context_menu_visible()) @@ -15285,7 +15285,13 @@ 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.trigger_completion_on_input("n", true, window, cx) + editor.show_completions( + &ShowCompletions { + trigger: Some("\n".into()), + }, + window, + cx, + ); }); cx.executor().run_until_parked(); @@ -15383,7 +15389,7 @@ int fn_branch(bool do_branch1, bool do_branch2); }))) }); cx.update_editor(|editor, window, cx| { - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); cx.executor().run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -15432,7 +15438,7 @@ int fn_branch(bool do_branch1, bool do_branch2); }))) }); cx.update_editor(|editor, window, cx| { - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); cx.executor().run_until_parked(); cx.update_editor(|editor, window, cx| { @@ -17922,7 +17928,7 @@ async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) { } }); cx.update_editor(|editor, window, cx| { - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); completion_requests.next().await; cx.condition(|editor, _| editor.context_menu_visible()) @@ -24318,7 +24324,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { ]))) }); editor.update_in(cx, |editor, window, cx| { - editor.show_completions(&ShowCompletions, window, cx); + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); cx.run_until_parked(); completion_handle.next().await.unwrap();