Eagerly attempt to resolve missing completion documentation

Julia created

Change summary

crates/editor/src/editor.rs | 260 ++++++++++++++++++++++++++++----------
1 file changed, 189 insertions(+), 71 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -25,7 +25,7 @@ use ::git::diff::DiffHunk;
 use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Context, Result};
 use blink_manager::BlinkManager;
-use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings};
+use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
 use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
@@ -62,8 +62,8 @@ use language::{
     language_settings::{self, all_language_settings, InlayHintSettings},
     markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
     Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind,
-    IndentSize, Language, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection,
-    SelectionGoal, TransactionId,
+    IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point,
+    Selection, SelectionGoal, TransactionId,
 };
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
@@ -954,7 +954,7 @@ impl CompletionsMenu {
     ) {
         self.selected_item = 0;
         self.list.scroll_to(ScrollTarget::Show(self.selected_item));
-        self.attempt_resolve_selected_completion(project, cx);
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
@@ -967,7 +967,7 @@ impl CompletionsMenu {
             self.selected_item -= 1;
             self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         }
-        self.attempt_resolve_selected_completion(project, cx);
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
@@ -980,7 +980,7 @@ impl CompletionsMenu {
             self.selected_item += 1;
             self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         }
-        self.attempt_resolve_selected_completion(project, cx);
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
@@ -991,16 +991,99 @@ impl CompletionsMenu {
     ) {
         self.selected_item = self.matches.len() - 1;
         self.list.scroll_to(ScrollTarget::Show(self.selected_item));
-        self.attempt_resolve_selected_completion(project, cx);
+        self.attempt_resolve_selected_completion_documentation(project, cx);
         cx.notify();
     }
 
-    fn attempt_resolve_selected_completion(
+    fn pre_resolve_completion_documentation(
+        &self,
+        project: Option<ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let Some(project) = project else {
+            return;
+        };
+        let client = project.read(cx).client();
+        let language_registry = project.read(cx).languages().clone();
+
+        let is_remote = project.read(cx).is_remote();
+        let project_id = project.read(cx).remote_id();
+
+        let completions = self.completions.clone();
+        let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
+
+        cx.spawn(move |this, mut cx| async move {
+            if is_remote {
+                let Some(project_id) = project_id else {
+                    log::error!("Remote project without remote_id");
+                    return;
+                };
+
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    Self::resolve_completion_documentation_remote(
+                        project_id,
+                        server_id,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        client.clone(),
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            } else {
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    let server = project.read_with(&mut cx, |project, _| {
+                        project.language_server_for_id(server_id)
+                    });
+                    let Some(server) = server else {
+                        return;
+                    };
+
+                    Self::resolve_completion_documentation_local(
+                        server,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            }
+        })
+        .detach();
+    }
+
+    fn attempt_resolve_selected_completion_documentation(
         &mut self,
         project: Option<&ModelHandle<Project>>,
         cx: &mut ViewContext<Editor>,
     ) {
-        let index = self.matches[self.selected_item].candidate_id;
+        let completion_index = self.matches[self.selected_item].candidate_id;
         let Some(project) = project else {
             return;
         };
@@ -1008,7 +1091,7 @@ impl CompletionsMenu {
 
         let completions = self.completions.clone();
         let completions_guard = completions.read();
-        let completion = &completions_guard[index];
+        let completion = &completions_guard[completion_index];
         if completion.documentation.is_some() {
             return;
         }
@@ -1024,54 +1107,95 @@ impl CompletionsMenu {
             };
 
             let client = project.read(cx).client();
-            let request = proto::ResolveCompletionDocumentation {
-                project_id,
-                language_server_id: server_id.0 as u64,
-                lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
-            };
-
-            cx.spawn(|this, mut cx| async move {
-                let Some(response) = client
-                    .request(request)
-                    .await
-                    .context("completion documentation resolve proto request")
-                    .log_err()
-                else {
-                    return;
-                };
 
-                if response.text.is_empty() {
-                    let mut completions = completions.write();
-                    let completion = &mut completions[index];
-                    completion.documentation = Some(Documentation::Undocumented);
-                }
+            cx.spawn(move |this, mut cx| async move {
+                Self::resolve_completion_documentation_remote(
+                    project_id,
+                    server_id,
+                    completions.clone(),
+                    completion_index,
+                    completion,
+                    client,
+                    language_registry.clone(),
+                )
+                .await;
 
-                let documentation = if response.is_markdown {
-                    Documentation::MultiLineMarkdown(
-                        markdown::parse_markdown(&response.text, &language_registry, None).await,
-                    )
-                } else if response.text.lines().count() <= 1 {
-                    Documentation::SingleLine(response.text)
-                } else {
-                    Documentation::MultiLinePlainText(response.text)
-                };
+                _ = this.update(&mut cx, |_, cx| cx.notify());
+            })
+            .detach();
+        } else {
+            let Some(server) = project.read(cx).language_server_for_id(server_id) else {
+                return;
+            };
 
-                let mut completions = completions.write();
-                let completion = &mut completions[index];
-                completion.documentation = Some(documentation);
-                drop(completions);
+            cx.spawn(move |this, mut cx| async move {
+                Self::resolve_completion_documentation_local(
+                    server,
+                    completions,
+                    completion_index,
+                    completion,
+                    language_registry,
+                )
+                .await;
 
                 _ = this.update(&mut cx, |_, cx| cx.notify());
             })
             .detach();
+        }
+    }
 
+    async fn resolve_completion_documentation_remote(
+        project_id: u64,
+        server_id: LanguageServerId,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        client: Arc<Client>,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
+        let request = proto::ResolveCompletionDocumentation {
+            project_id,
+            language_server_id: server_id.0 as u64,
+            lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
+        };
+
+        let Some(response) = client
+            .request(request)
+            .await
+            .context("completion documentation resolve proto request")
+            .log_err()
+        else {
             return;
+        };
+
+        if response.text.is_empty() {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
         }
 
-        let Some(server) = project.read(cx).language_server_for_id(server_id) else {
-            return;
+        let documentation = if response.is_markdown {
+            Documentation::MultiLineMarkdown(
+                markdown::parse_markdown(&response.text, &language_registry, None).await,
+            )
+        } else if response.text.lines().count() <= 1 {
+            Documentation::SingleLine(response.text)
+        } else {
+            Documentation::MultiLinePlainText(response.text)
         };
 
+        let mut completions = completions.write();
+        let completion = &mut completions[completion_index];
+        completion.documentation = Some(documentation);
+    }
+
+    async fn resolve_completion_documentation_local(
+        server: Arc<lsp::LanguageServer>,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
         let can_resolve = server
             .capabilities()
             .completion_provider
@@ -1082,33 +1206,27 @@ impl CompletionsMenu {
             return;
         }
 
-        cx.spawn(|this, mut cx| async move {
-            let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
-            let Some(completion_item) = request.await.log_err() else {
-                return;
-            };
-
-            if let Some(lsp_documentation) = completion_item.documentation {
-                let documentation = language::prepare_completion_documentation(
-                    &lsp_documentation,
-                    &language_registry,
-                    None, // TODO: Try to reasonably work out which language the completion is for
-                )
-                .await;
+        let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+        let Some(completion_item) = request.await.log_err() else {
+            return;
+        };
 
-                let mut completions = completions.write();
-                let completion = &mut completions[index];
-                completion.documentation = Some(documentation);
-                drop(completions);
+        if let Some(lsp_documentation) = completion_item.documentation {
+            let documentation = language::prepare_completion_documentation(
+                &lsp_documentation,
+                &language_registry,
+                None, // TODO: Try to reasonably work out which language the completion is for
+            )
+            .await;
 
-                _ = this.update(&mut cx, |_, cx| cx.notify());
-            } else {
-                let mut completions = completions.write();
-                let completion = &mut completions[index];
-                completion.documentation = Some(Documentation::Undocumented);
-            }
-        })
-        .detach();
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(documentation);
+        } else {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
     }
 
     fn visible(&self) -> bool {
@@ -3450,7 +3568,7 @@ impl Editor {
                         None
                     } else {
                         _ = this.update(&mut cx, |editor, cx| {
-                            menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx);
+                            menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
                         });
                         Some(menu)
                     }