Most of getting completion documentation resolved & cached MD parsing

Julia created

Change summary

Cargo.lock                         |   2 
crates/editor/Cargo.toml           |   1 
crates/editor/src/editor.rs        | 170 +++++++++++++++++++++++++------
crates/editor/src/hover_popover.rs |   6 
crates/language/Cargo.toml         |   1 
crates/language/src/buffer.rs      |   2 
crates/language/src/language.rs    |   1 
crates/language/src/markdown.rs    |  90 +++++++++------
crates/language/src/proto.rs       |   1 
crates/lsp/src/lsp.rs              |  14 ++
crates/project/src/lsp_command.rs  |   1 
crates/zed/src/languages.rs        |   4 
12 files changed, 217 insertions(+), 76 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2404,7 +2404,6 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "project",
- "pulldown-cmark",
  "rand 0.8.5",
  "rich_text",
  "rpc",
@@ -3990,6 +3989,7 @@ dependencies = [
  "lsp",
  "parking_lot 0.11.2",
  "postage",
+ "pulldown-cmark",
  "rand 0.8.5",
  "regex",
  "rpc",

crates/editor/Cargo.toml 🔗

@@ -57,7 +57,6 @@ log.workspace = true
 ordered-float.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-pulldown-cmark = { version = "0.9.2", default-features = false }
 rand.workspace = true
 schemars.workspace = true
 serde.workspace = true

crates/editor/src/editor.rs 🔗

@@ -9,7 +9,6 @@ mod highlight_matching_bracket;
 mod hover_popover;
 pub mod items;
 mod link_go_to_definition;
-mod markdown;
 mod mouse_context_menu;
 pub mod movement;
 pub mod multi_buffer;
@@ -78,6 +77,7 @@ pub use multi_buffer::{
     ToPoint,
 };
 use ordered_float::OrderedFloat;
+use parking_lot::RwLock;
 use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::{seq::SliceRandom, thread_rng};
 use rpc::proto::PeerId;
@@ -788,10 +788,14 @@ enum ContextMenu {
 }
 
 impl ContextMenu {
-    fn select_first(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_first(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_first(cx),
+                ContextMenu::Completions(menu) => menu.select_first(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_first(cx),
             }
             true
@@ -800,10 +804,14 @@ impl ContextMenu {
         }
     }
 
-    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_prev(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_prev(cx),
+                ContextMenu::Completions(menu) => menu.select_prev(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_prev(cx),
             }
             true
@@ -812,10 +820,14 @@ impl ContextMenu {
         }
     }
 
-    fn select_next(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_next(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_next(cx),
+                ContextMenu::Completions(menu) => menu.select_next(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_next(cx),
             }
             true
@@ -824,10 +836,14 @@ impl ContextMenu {
         }
     }
 
-    fn select_last(&mut self, cx: &mut ViewContext<Editor>) -> bool {
+    fn select_last(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_last(cx),
+                ContextMenu::Completions(menu) => menu.select_last(project, cx),
                 ContextMenu::CodeActions(menu) => menu.select_last(cx),
             }
             true
@@ -861,7 +877,7 @@ struct CompletionsMenu {
     id: CompletionId,
     initial_position: Anchor,
     buffer: ModelHandle<Buffer>,
-    completions: Arc<[Completion]>,
+    completions: Arc<RwLock<Box<[Completion]>>>,
     match_candidates: Vec<StringMatchCandidate>,
     matches: Arc<[StringMatch]>,
     selected_item: usize,
@@ -869,34 +885,115 @@ struct CompletionsMenu {
 }
 
 impl CompletionsMenu {
-    fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_first(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         self.selected_item = 0;
         self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion(project, cx);
         cx.notify();
     }
 
-    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_prev(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
             self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         }
+        self.attempt_resolve_selected_completion(project, cx);
         cx.notify();
     }
 
-    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_next(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         if self.selected_item + 1 < self.matches.len() {
             self.selected_item += 1;
             self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         }
+        self.attempt_resolve_selected_completion(project, cx);
         cx.notify();
     }
 
-    fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
+    fn select_last(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
         self.selected_item = self.matches.len() - 1;
         self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        self.attempt_resolve_selected_completion(project, cx);
         cx.notify();
     }
 
+    fn attempt_resolve_selected_completion(
+        &mut self,
+        project: Option<&ModelHandle<Project>>,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        println!("attempt_resolve_selected_completion");
+        let index = self.matches[dbg!(self.selected_item)].candidate_id;
+        dbg!(index);
+        let Some(project) = project else {
+            println!("no project");
+            return;
+        };
+
+        let completions = self.completions.clone();
+        let completions_guard = completions.read();
+        let completion = &completions_guard[index];
+        if completion.lsp_completion.documentation.is_some() {
+            println!("has existing documentation");
+            return;
+        }
+
+        let server_id = completion.server_id;
+        let completion = completion.lsp_completion.clone();
+        drop(completions_guard);
+
+        let Some(server) = project.read(cx).language_server_for_id(server_id) else {
+            println!("no server");
+            return;
+        };
+
+        let can_resolve = server
+            .capabilities()
+            .completion_provider
+            .as_ref()
+            .and_then(|options| options.resolve_provider)
+            .unwrap_or(false);
+        if !dbg!(can_resolve) {
+            return;
+        }
+
+        cx.spawn(|this, mut cx| async move {
+            println!("in spawn");
+            let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+            let Some(completion_item) = request.await.log_err() else {
+                println!("errored");
+                return;
+            };
+
+            if completion_item.documentation.is_some() {
+                println!("got new documentation");
+                let mut completions = completions.write();
+                completions[index].lsp_completion.documentation = completion_item.documentation;
+                println!("notifying");
+                _ = this.update(&mut cx, |_, cx| cx.notify());
+            } else {
+                println!("did not get anything");
+            }
+        })
+        .detach();
+    }
+
     fn visible(&self) -> bool {
         !self.matches.is_empty()
     }
@@ -914,7 +1011,8 @@ impl CompletionsMenu {
             .iter()
             .enumerate()
             .max_by_key(|(_, mat)| {
-                let completion = &self.completions[mat.candidate_id];
+                let completions = self.completions.read();
+                let completion = &completions[mat.candidate_id];
                 let documentation = &completion.lsp_completion.documentation;
 
                 let mut len = completion.label.text.chars().count();
@@ -938,6 +1036,7 @@ impl CompletionsMenu {
             let style = style.clone();
             move |_, range, items, cx| {
                 let start_ix = range.start;
+                let completions = completions.read();
                 for (ix, mat) in matches[range].iter().enumerate() {
                     let completion = &completions[mat.candidate_id];
                     let documentation = &completion.lsp_completion.documentation;
@@ -1052,7 +1151,8 @@ impl CompletionsMenu {
             .with_child(list)
             .with_children({
                 let mat = &self.matches[selected_item];
-                let completion = &self.completions[mat.candidate_id];
+                let completions = self.completions.read();
+                let completion = &completions[mat.candidate_id];
                 let documentation = &completion.lsp_completion.documentation;
 
                 if let Some(lsp::Documentation::MarkupContent(content)) = documentation {
@@ -1069,13 +1169,12 @@ impl CompletionsMenu {
                     Some(
                         Flex::column()
                             .scrollable::<CompletionDocsMarkdown>(0, None, cx)
-                            .with_child(crate::markdown::render_markdown(
-                                &content.value,
-                                &registry,
-                                &language,
-                                &style,
-                                cx,
-                            ))
+                            // .with_child(language::markdown::render_markdown(
+                            //     &content.value,
+                            //     &registry,
+                            //     &language,
+                            //     &style,
+                            // ))
                             .constrained()
                             .with_width(alongside_docs_width)
                             .contained()
@@ -1130,17 +1229,20 @@ impl CompletionsMenu {
             }
         }
 
+        let completions = self.completions.read();
         matches.sort_unstable_by_key(|mat| {
-            let completion = &self.completions[mat.candidate_id];
+            let completion = &completions[mat.candidate_id];
             (
                 completion.lsp_completion.sort_text.as_ref(),
                 Reverse(OrderedFloat(mat.score)),
                 completion.sort_key(),
             )
         });
+        drop(completions);
 
         for mat in &mut matches {
-            let filter_start = self.completions[mat.candidate_id].label.filter_range.start;
+            let completions = self.completions.read();
+            let filter_start = completions[mat.candidate_id].label.filter_range.start;
             for position in &mut mat.positions {
                 *position += filter_start;
             }
@@ -3187,7 +3289,7 @@ impl Editor {
                             })
                             .collect(),
                         buffer,
-                        completions: completions.into(),
+                        completions: Arc::new(RwLock::new(completions.into())),
                         matches: Vec::new().into(),
                         selected_item: 0,
                         list: Default::default(),
@@ -3196,6 +3298,9 @@ impl Editor {
                     if menu.matches.is_empty() {
                         None
                     } else {
+                        _ = this.update(&mut cx, |editor, cx| {
+                            menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx);
+                        });
                         Some(menu)
                     }
                 } else {
@@ -3252,7 +3357,8 @@ impl Editor {
             .matches
             .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
         let buffer_handle = completions_menu.buffer;
-        let completion = completions_menu.completions.get(mat.candidate_id)?;
+        let completions = completions_menu.completions.read();
+        let completion = completions.get(mat.candidate_id)?;
 
         let snippet;
         let text;
@@ -5372,7 +5478,7 @@ impl Editor {
         if self
             .context_menu
             .as_mut()
-            .map(|menu| menu.select_last(cx))
+            .map(|menu| menu.select_last(self.project.as_ref(), cx))
             .unwrap_or(false)
         {
             return;
@@ -5416,25 +5522,25 @@ impl Editor {
 
     pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_first(cx);
+            context_menu.select_first(self.project.as_ref(), cx);
         }
     }
 
     pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_prev(cx);
+            context_menu.select_prev(self.project.as_ref(), cx);
         }
     }
 
     pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_next(cx);
+            context_menu.select_next(self.project.as_ref(), cx);
         }
     }
 
     pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_last(cx);
+            context_menu.select_last(self.project.as_ref(), cx);
         }
     }
 

crates/editor/src/hover_popover.rs 🔗

@@ -1,7 +1,6 @@
 use crate::{
     display_map::{InlayOffset, ToDisplayPoint},
     link_go_to_definition::{DocumentRange, InlayRange},
-    markdown::{self, RenderedRegion},
     Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
     ExcerptId, RangeToAnchorExt,
 };
@@ -13,7 +12,10 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
 };
-use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
+use language::{
+    markdown::{self, RenderedRegion},
+    Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry,
+};
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
 use std::{ops::Range, sync::Arc, time::Duration};
 use util::TryFutureExt;

crates/language/Cargo.toml 🔗

@@ -46,6 +46,7 @@ lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
+pulldown-cmark = { version = "0.9.2", default-features = false }
 regex.workspace = true
 schemars.workspace = true
 serde.workspace = true

crates/language/src/buffer.rs 🔗

@@ -1,6 +1,7 @@
 pub use crate::{
     diagnostic_set::DiagnosticSet,
     highlight_map::{HighlightId, HighlightMap},
+    markdown::RenderedMarkdown,
     proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
 };
 use crate::{
@@ -148,6 +149,7 @@ pub struct Completion {
     pub old_range: Range<Anchor>,
     pub new_text: String,
     pub label: CodeLabel,
+    pub alongside_documentation: Option<RenderedMarkdown>,
     pub server_id: LanguageServerId,
     pub lsp_completion: lsp::CompletionItem,
 }

crates/language/src/language.rs 🔗

@@ -2,6 +2,7 @@ mod buffer;
 mod diagnostic_set;
 mod highlight_map;
 pub mod language_settings;
+pub mod markdown;
 mod outline;
 pub mod proto;
 mod syntax_map;

crates/editor/src/markdown.rs → crates/language/src/markdown.rs 🔗

@@ -1,6 +1,7 @@
 use std::ops::Range;
 use std::sync::Arc;
 
+use crate::{Language, LanguageRegistry};
 use futures::FutureExt;
 use gpui::{
     elements::Text,
@@ -8,10 +9,50 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     CursorRegion, MouseRegion, ViewContext,
 };
-use language::{Language, LanguageRegistry};
 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 
-use crate::{Editor, EditorStyle};
+#[derive(Debug, Clone)]
+pub struct RenderedMarkdown {
+    text: String,
+    highlights: Vec<(Range<usize>, HighlightStyle)>,
+    region_ranges: Vec<Range<usize>>,
+    regions: Vec<RenderedRegion>,
+}
+
+// impl RenderedMarkdown {
+//     pub fn render(&self, style: &theme::Editor, cx: &mut ViewContext<Editor>) -> Text {
+//         let code_span_background_color = style.document_highlight_read_background;
+//         let view_id = cx.view_id();
+//         let mut region_id = 0;
+//         Text::new(text, style.text.clone())
+//             .with_highlights(highlights)
+//             .with_custom_runs(region_ranges, move |ix, bounds, scene, _| {
+//                 region_id += 1;
+//                 let region = regions[ix].clone();
+//                 if let Some(url) = region.link_url {
+//                     scene.push_cursor_region(CursorRegion {
+//                         bounds,
+//                         style: CursorStyle::PointingHand,
+//                     });
+//                     scene.push_mouse_region(
+//                         MouseRegion::new::<Editor>(view_id, region_id, bounds)
+//                             .on_click::<Editor, _>(MouseButton::Left, move |_, _, cx| {
+//                                 cx.platform().open_url(&url)
+//                             }),
+//                     );
+//                 }
+//                 if region.code {
+//                     scene.push_quad(gpui::Quad {
+//                         bounds,
+//                         background: Some(code_span_background_color),
+//                         border: Default::default(),
+//                         corner_radii: (2.0).into(),
+//                     });
+//                 }
+//             })
+//             .with_soft_wrap(true)
+//     }
+// }
 
 #[derive(Debug, Clone)]
 pub struct RenderedRegion {
@@ -23,9 +64,8 @@ pub fn render_markdown(
     markdown: &str,
     language_registry: &Arc<LanguageRegistry>,
     language: &Option<Arc<Language>>,
-    style: &EditorStyle,
-    cx: &mut ViewContext<Editor>,
-) -> Text {
+    style: &theme::Editor,
+) -> RenderedMarkdown {
     let mut text = String::new();
     let mut highlights = Vec::new();
     let mut region_ranges = Vec::new();
@@ -42,43 +82,19 @@ pub fn render_markdown(
         &mut regions,
     );
 
-    let code_span_background_color = style.document_highlight_read_background;
-    let view_id = cx.view_id();
-    let mut region_id = 0;
-    Text::new(text, style.text.clone())
-        .with_highlights(highlights)
-        .with_custom_runs(region_ranges, move |ix, bounds, scene, _| {
-            region_id += 1;
-            let region = regions[ix].clone();
-            if let Some(url) = region.link_url {
-                scene.push_cursor_region(CursorRegion {
-                    bounds,
-                    style: CursorStyle::PointingHand,
-                });
-                scene.push_mouse_region(
-                    MouseRegion::new::<Editor>(view_id, region_id, bounds)
-                        .on_click::<Editor, _>(MouseButton::Left, move |_, _, cx| {
-                            cx.platform().open_url(&url)
-                        }),
-                );
-            }
-            if region.code {
-                scene.push_quad(gpui::Quad {
-                    bounds,
-                    background: Some(code_span_background_color),
-                    border: Default::default(),
-                    corner_radii: (2.0).into(),
-                });
-            }
-        })
-        .with_soft_wrap(true)
+    RenderedMarkdown {
+        text,
+        highlights,
+        region_ranges,
+        regions,
+    }
 }
 
 pub fn render_markdown_block(
     markdown: &str,
     language_registry: &Arc<LanguageRegistry>,
     language: &Option<Arc<Language>>,
-    style: &EditorStyle,
+    style: &theme::Editor,
     text: &mut String,
     highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
     region_ranges: &mut Vec<Range<usize>>,
@@ -231,7 +247,7 @@ pub fn render_code(
     highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
     content: &str,
     language: &Arc<Language>,
-    style: &EditorStyle,
+    style: &theme::Editor,
 ) {
     let prev_len = text.len();
     text.push_str(content);

crates/language/src/proto.rs 🔗

@@ -482,6 +482,7 @@ pub async fn deserialize_completion(
                 lsp_completion.filter_text.as_deref(),
             )
         }),
+        alongside_documentation: None,
         server_id: LanguageServerId(completion.server_id as usize),
         lsp_completion,
     })

crates/lsp/src/lsp.rs 🔗

@@ -466,7 +466,10 @@ impl LanguageServer {
                         completion_item: Some(CompletionItemCapability {
                             snippet_support: Some(true),
                             resolve_support: Some(CompletionItemCapabilityResolveSupport {
-                                properties: vec!["additionalTextEdits".to_string()],
+                                properties: vec![
+                                    "documentation".to_string(),
+                                    "additionalTextEdits".to_string(),
+                                ],
                             }),
                             ..Default::default()
                         }),
@@ -748,6 +751,15 @@ impl LanguageServer {
         )
     }
 
+    // some child of string literal (be it "" or ``) which is the child of an attribute
+
+    // <Foo className="bar" />
+    // <Foo className={`bar`} />
+    // <Foo className={something + "bar"} />
+    // <Foo className={something + "bar"} />
+    // const classes = "awesome ";
+    // <Foo className={classes} />
+
     fn request_internal<T: request::Request>(
         next_id: &AtomicUsize,
         response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,

crates/project/src/lsp_command.rs 🔗

@@ -1462,6 +1462,7 @@ impl LspCommand for GetCompletions {
                                     lsp_completion.filter_text.as_deref(),
                                 )
                             }),
+                            alongside_documentation: None,
                             server_id,
                             lsp_completion,
                         }

crates/zed/src/languages.rs 🔗

@@ -128,8 +128,8 @@ pub fn init(
         "tsx",
         tree_sitter_typescript::language_tsx(),
         vec![
-            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
-            Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+            // Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            // Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
             Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
         ],
     );