lsp: Provide completion reason in the request (#12893)

Piotr Osiewicz created

This should help LS make a better call about the completions it should
return back to the caller. For example, it speeds up import completions
for typescript.
Before: 


https://github.com/zed-industries/zed/assets/24362066/b38fd565-f9ff-4db7-a87f-c3b31a9fdc96

after: 


https://github.com/zed-industries/zed/assets/24362066/d4fbc9ae-9aab-4543-b9f6-16acf1619576


This should be merged after 06.12 Preview to give it some time on
Nightly.

Release Notes:

- N/A

Change summary

crates/assistant/src/assistant_panel.rs                       |  2 
crates/assistant/src/slash_command.rs                         |  1 
crates/collab/src/tests/random_project_collaboration_tests.rs |  6 
crates/collab_ui/src/chat_panel/message_editor.rs             |  1 
crates/editor/src/actions.rs                                  |  7 
crates/editor/src/editor.rs                                   | 44 ++++
crates/editor/src/editor_tests.rs                             |  2 
crates/extension/src/extension_store_test.rs                  |  6 
crates/lsp/src/lsp.rs                                         |  2 
crates/project/src/lsp_command.rs                             | 16 +
crates/project/src/project.rs                                 | 19 +
crates/project/src/project_tests.rs                           |  6 
12 files changed, 86 insertions(+), 26 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -2350,7 +2350,7 @@ impl ContextEditor {
                     editor.insert(&format!("/{name}"), cx);
                     if command.requires_argument() {
                         editor.insert(" ", cx);
-                        editor.show_completions(&ShowCompletions, cx);
+                        editor.show_completions(&ShowCompletions::default(), cx);
                     }
                 });
             });

crates/assistant/src/slash_command.rs 🔗

@@ -217,6 +217,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
         &self,
         buffer: &Model<Buffer>,
         buffer_position: Anchor,
+        _: editor::CompletionContext,
         cx: &mut ViewContext<Editor>,
     ) -> Task<Result<Vec<project::Completion>>> {
         let Some((name, argument, command_range, argument_range)) =

crates/collab/src/tests/random_project_collaboration_tests.rs 🔗

@@ -14,7 +14,9 @@ use language::{
 };
 use lsp::FakeLanguageServer;
 use pretty_assertions::assert_eq;
-use project::{search::SearchQuery, Project, ProjectPath, SearchResult};
+use project::{
+    search::SearchQuery, Project, ProjectPath, SearchResult, DEFAULT_COMPLETION_CONTEXT,
+};
 use rand::{
     distributions::{Alphanumeric, DistString},
     prelude::*,
@@ -829,7 +831,7 @@ impl RandomizedTest for ProjectCollaborationTest {
                         .map_ok(|_| ())
                         .boxed(),
                     LspRequestKind::Completion => project
-                        .completions(&buffer, offset, cx)
+                        .completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
                         .map_ok(|_| ())
                         .boxed(),
                     LspRequestKind::CodeAction => project

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

@@ -46,6 +46,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
         &self,
         buffer: &Model<Buffer>,
         buffer_position: language::Anchor,
+        _: editor::CompletionContext,
         cx: &mut ViewContext<Editor>,
     ) -> Task<anyhow::Result<Vec<Completion>>> {
         let Some(handle) = self.0.upgrade() else {

crates/editor/src/actions.rs 🔗

@@ -125,6 +125,11 @@ pub struct ExpandExcerptsDown {
     #[serde(default)]
     pub(super) lines: u32,
 }
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct ShowCompletions {
+    #[serde(default)]
+    pub(super) trigger: Option<char>,
+}
 
 impl_actions!(
     editor,
@@ -147,6 +152,7 @@ impl_actions!(
         SelectToBeginningOfLine,
         SelectToEndOfLine,
         SelectUpByLines,
+        ShowCompletions,
         ToggleCodeActions,
         ToggleComments,
         UnfoldAt,
@@ -274,7 +280,6 @@ gpui::actions!(
         SelectToStartOfParagraph,
         SelectUp,
         ShowCharacterPalette,
-        ShowCompletions,
         ShowInlineCompletion,
         ShuffleLines,
         SortLinesCaseInsensitive,

crates/editor/src/editor.rs 🔗

@@ -93,7 +93,8 @@ use linked_editing_ranges::refresh_linked_ranges;
 use task::{ResolvedTask, TaskTemplate, TaskVariables};
 
 use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
-use lsp::{DiagnosticSeverity, LanguageServerId};
+pub use lsp::CompletionContext;
+use lsp::{CompletionTriggerKind, DiagnosticSeverity, LanguageServerId};
 use mouse_context_menu::MouseContextMenu;
 use movement::TextLayoutDetails;
 pub use multi_buffer::{
@@ -2300,7 +2301,7 @@ impl Editor {
                     .detach();
 
                     if show_completions {
-                        self.show_completions(&ShowCompletions, cx);
+                        self.show_completions(&ShowCompletions { trigger: None }, cx);
                     }
                 } else {
                     drop(context_menu);
@@ -3494,7 +3495,12 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if self.is_completion_trigger(text, trigger_in_words, cx) {
-            self.show_completions(&ShowCompletions, cx);
+            self.show_completions(
+                &ShowCompletions {
+                    trigger: text.chars().last(),
+                },
+                cx,
+            );
         } else {
             self.hide_context_menu(cx);
         }
@@ -3890,7 +3896,7 @@ impl Editor {
         }))
     }
 
-    pub fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
+    pub fn show_completions(&mut self, options: &ShowCompletions, cx: &mut ViewContext<Self>) {
         if self.pending_rename.is_some() {
             return;
         }
@@ -3908,7 +3914,29 @@ impl Editor {
             };
 
         let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
-        let completions = provider.completions(&buffer, buffer_position, cx);
+        let is_followup_invoke = {
+            let context_menu_state = self.context_menu.read();
+            matches!(
+                context_menu_state.deref(),
+                Some(ContextMenu::Completions(_))
+            )
+        };
+        let trigger_kind = match (options.trigger, is_followup_invoke) {
+            (_, true) => CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
+            (Some(_), _) => CompletionTriggerKind::TRIGGER_CHARACTER,
+            _ => CompletionTriggerKind::INVOKED,
+        };
+        let completion_context = CompletionContext {
+            trigger_character: options.trigger.and_then(|c| {
+                if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
+                    Some(String::from(c))
+                } else {
+                    None
+                }
+            }),
+            trigger_kind,
+        };
+        let completions = provider.completions(&buffer, buffer_position, completion_context, cx);
 
         let id = post_inc(&mut self.next_completion_id);
         let task = cx.spawn(|this, mut cx| {
@@ -4166,7 +4194,7 @@ impl Editor {
         }
 
         if completion.show_new_completions_on_confirm {
-            self.show_completions(&ShowCompletions, cx);
+            self.show_completions(&ShowCompletions { trigger: None }, cx);
         }
 
         let provider = self.completion_provider.as_ref()?;
@@ -11428,6 +11456,7 @@ pub trait CompletionProvider {
         &self,
         buffer: &Model<Buffer>,
         buffer_position: text::Anchor,
+        trigger: CompletionContext,
         cx: &mut ViewContext<Editor>,
     ) -> Task<Result<Vec<Completion>>>;
 
@@ -11462,10 +11491,11 @@ impl CompletionProvider for Model<Project> {
         &self,
         buffer: &Model<Buffer>,
         buffer_position: text::Anchor,
+        options: CompletionContext,
         cx: &mut ViewContext<Editor>,
     ) -> Task<Result<Vec<Completion>>> {
         self.update(cx, |project, cx| {
-            project.completions(&buffer, buffer_position, cx)
+            project.completions(&buffer, buffer_position, options, cx)
         })
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -6705,7 +6705,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
     cx.assert_editor_state("editor.cloˇ");
     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
     cx.update_editor(|editor, cx| {
-        editor.show_completions(&ShowCompletions, cx);
+        editor.show_completions(&ShowCompletions { trigger: None }, cx);
     });
     handle_completion_request(
         &mut cx,

crates/extension/src/extension_store_test.rs 🔗

@@ -15,7 +15,7 @@ use http::{FakeHttpClient, Response};
 use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
 use node_runtime::FakeNodeRuntime;
 use parking_lot::Mutex;
-use project::Project;
+use project::{Project, DEFAULT_COMPLETION_CONTEXT};
 use serde_json::json;
 use settings::{Settings as _, SettingsStore};
 use std::{
@@ -657,7 +657,9 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
     });
 
     let completion_labels = project
-        .update(cx, |project, cx| project.completions(&buffer, 0, cx))
+        .update(cx, |project, cx| {
+            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
+        })
         .await
         .unwrap()
         .into_iter()

crates/lsp/src/lsp.rs 🔗

@@ -1061,7 +1061,7 @@ impl LanguageServer {
             select! {
                 response = rx.fuse() => {
                     let elapsed = started.elapsed();
-                    log::trace!("Took {elapsed:?} to receive response to {method:?} id {id}");
+                    log::info!("Took {elapsed:?} to receive response to {method:?} id {id}");
                     cancel_on_drop.abort();
                     response?
                 }

crates/project/src/lsp_command.rs 🔗

@@ -16,8 +16,9 @@ use language::{
     OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use lsp::{
-    CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
-    LinkedEditingRangeServerCapabilities, OneOf, ServerCapabilities,
+    CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
+    DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
+    OneOf, ServerCapabilities,
 };
 use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
 use text::{BufferId, LineEnding};
@@ -127,6 +128,7 @@ pub(crate) struct GetHover {
 
 pub(crate) struct GetCompletions {
     pub position: PointUtf16,
+    pub context: CompletionContext,
 }
 
 #[derive(Clone)]
@@ -1464,7 +1466,7 @@ impl LspCommand for GetCompletions {
                 lsp::TextDocumentIdentifier::new(lsp::Uri::from_file_path(path).unwrap().into()),
                 point_to_lsp(self.position),
             ),
-            context: Default::default(),
+            context: Some(self.context.clone()),
             work_done_progress_params: Default::default(),
             partial_result_params: Default::default(),
         }
@@ -1649,7 +1651,13 @@ impl LspCommand for GetCompletions {
                 })
             })
             .ok_or_else(|| anyhow!("invalid position"))??;
-        Ok(Self { position })
+        Ok(Self {
+            position,
+            context: CompletionContext {
+                trigger_kind: CompletionTriggerKind::INVOKED,
+                trigger_character: None,
+            },
+        })
     }
 
     fn response_to_proto(

crates/project/src/project.rs 🔗

@@ -58,7 +58,7 @@ use language::{
 };
 use log::error;
 use lsp::{
-    DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
+    CompletionContext, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
     DocumentHighlightKind, Edit, FileSystemWatcher, LanguageServer, LanguageServerBinary,
     LanguageServerId, LspRequestFuture, MessageActionItem, OneOf, ServerCapabilities,
     ServerHealthStatus, ServerStatus, TextEdit, Uri,
@@ -651,6 +651,12 @@ pub enum SearchResult {
     LimitReached,
 }
 
+#[cfg(any(test, feature = "test-support"))]
+pub const DEFAULT_COMPLETION_CONTEXT: CompletionContext = CompletionContext {
+    trigger_kind: lsp::CompletionTriggerKind::INVOKED,
+    trigger_character: None,
+};
+
 impl Project {
     pub fn init_settings(cx: &mut AppContext) {
         WorktreeSettings::register(cx);
@@ -5875,6 +5881,7 @@ impl Project {
         &self,
         buffer: &Model<Buffer>,
         position: PointUtf16,
+        context: CompletionContext,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>> {
         let language_registry = self.languages.clone();
@@ -5908,7 +5915,10 @@ impl Project {
                             this.request_lsp(
                                 buffer.clone(),
                                 LanguageServerToQuery::Other(server_id),
-                                GetCompletions { position },
+                                GetCompletions {
+                                    position,
+                                    context: context.clone(),
+                                },
                                 cx,
                             ),
                         ));
@@ -5935,7 +5945,7 @@ impl Project {
             let task = self.send_lsp_proto_request(
                 buffer.clone(),
                 project_id,
-                GetCompletions { position },
+                GetCompletions { position, context },
                 cx,
             );
             let language = buffer.read(cx).language().cloned();
@@ -5969,10 +5979,11 @@ impl Project {
         &self,
         buffer: &Model<Buffer>,
         position: T,
+        context: CompletionContext,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>> {
         let position = position.to_point_utf16(buffer.read(cx));
-        self.completions_impl(buffer, position, cx)
+        self.completions_impl(buffer, position, context, cx)
     }
 
     pub fn resolve_completions(

crates/project/src/project_tests.rs 🔗

@@ -2499,7 +2499,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
     let text = "let a = b.fqn";
     buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
     let completions = project.update(cx, |project, cx| {
-        project.completions(&buffer, text.len(), cx)
+        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
     });
 
     fake_server
@@ -2526,7 +2526,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
     let text = "let a = \"atoms/cmp\"";
     buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
     let completions = project.update(cx, |project, cx| {
-        project.completions(&buffer, text.len() - 1, cx)
+        project.completions(&buffer, text.len() - 1, DEFAULT_COMPLETION_CONTEXT, cx)
     });
 
     fake_server
@@ -2591,7 +2591,7 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
     let text = "let a = b.fqn";
     buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
     let completions = project.update(cx, |project, cx| {
-        project.completions(&buffer, text.len(), cx)
+        project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
     });
 
     fake_server