Preparse documentation markdown when resolving completion

Julia created

Change summary

crates/editor/src/editor.rs        | 133 +++++++++++++++++--------------
crates/editor/src/hover_popover.rs |  26 +++---
crates/language/src/buffer.rs      |  44 ++++++++++
crates/language/src/markdown.rs    |  30 +++---
crates/language/src/proto.rs       |   2 
crates/project/src/lsp_command.rs  |   2 
6 files changed, 145 insertions(+), 92 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -119,12 +119,12 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
-pub fn render_rendered_markdown(
-    md: &language::RenderedMarkdown,
+pub fn render_parsed_markdown(
+    md: &language::ParsedMarkdown,
     style: &EditorStyle,
     cx: &mut ViewContext<Editor>,
 ) -> Text {
-    enum RenderedRenderedMarkdown {}
+    enum RenderedMarkdown {}
 
     let md = md.clone();
     let code_span_background_color = style.document_highlight_read_background;
@@ -141,7 +141,7 @@ pub fn render_rendered_markdown(
                     style: CursorStyle::PointingHand,
                 });
                 scene.push_mouse_region(
-                    MouseRegion::new::<RenderedRenderedMarkdown>(view_id, region_id, bounds)
+                    MouseRegion::new::<RenderedMarkdown>(view_id, region_id, bounds)
                         .on_click::<Editor, _>(MouseButton::Left, move |_, _, cx| {
                             cx.platform().open_url(&url)
                         }),
@@ -831,11 +831,12 @@ impl ContextMenu {
     fn select_first(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_first(project, cx),
+                ContextMenu::Completions(menu) => menu.select_first(project, style, cx),
                 ContextMenu::CodeActions(menu) => menu.select_first(cx),
             }
             true
@@ -847,11 +848,12 @@ impl ContextMenu {
     fn select_prev(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_prev(project, cx),
+                ContextMenu::Completions(menu) => menu.select_prev(project, style, cx),
                 ContextMenu::CodeActions(menu) => menu.select_prev(cx),
             }
             true
@@ -863,11 +865,12 @@ impl ContextMenu {
     fn select_next(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_next(project, cx),
+                ContextMenu::Completions(menu) => menu.select_next(project, style, cx),
                 ContextMenu::CodeActions(menu) => menu.select_next(cx),
             }
             true
@@ -879,11 +882,12 @@ impl ContextMenu {
     fn select_last(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         if self.visible() {
             match self {
-                ContextMenu::Completions(menu) => menu.select_last(project, cx),
+                ContextMenu::Completions(menu) => menu.select_last(project, style, cx),
                 ContextMenu::CodeActions(menu) => menu.select_last(cx),
             }
             true
@@ -928,60 +932,66 @@ impl CompletionsMenu {
     fn select_first(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         cx: &mut ViewContext<Editor>,
     ) {
         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(project, style, cx);
         cx.notify();
     }
 
     fn select_prev(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         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);
+        self.attempt_resolve_selected_completion(project, style, cx);
         cx.notify();
     }
 
     fn select_next(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         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);
+        self.attempt_resolve_selected_completion(project, style, cx);
         cx.notify();
     }
 
     fn select_last(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         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);
+        self.attempt_resolve_selected_completion(project, style, cx);
         cx.notify();
     }
 
     fn attempt_resolve_selected_completion(
         &mut self,
         project: Option<&ModelHandle<Project>>,
+        style: theme::Editor,
         cx: &mut ViewContext<Editor>,
     ) {
         let index = self.matches[self.selected_item].candidate_id;
         let Some(project) = project else {
             return;
         };
+        let language_registry = project.read(cx).languages().clone();
 
         let completions = self.completions.clone();
         let completions_guard = completions.read();
@@ -1008,16 +1018,27 @@ impl CompletionsMenu {
             return;
         }
 
+        // TODO: Do on background
         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 completion_item.documentation.is_some() {
+            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
+                    &style,
+                );
+
                 let mut completions = completions.write();
-                completions[index].lsp_completion.documentation = completion_item.documentation;
+                let completion = &mut completions[index];
+                completion.documentation = documentation;
+                completion.lsp_completion.documentation = Some(lsp_documentation);
                 drop(completions);
+
                 _ = this.update(&mut cx, |_, cx| cx.notify());
             }
         })
@@ -1069,7 +1090,7 @@ impl CompletionsMenu {
                 let completions = completions.read();
                 for (ix, mat) in matches[range].iter().enumerate() {
                     let completion = &completions[mat.candidate_id];
-                    let documentation = &completion.lsp_completion.documentation;
+                    let documentation = &completion.documentation;
                     let item_ix = start_ix + ix;
 
                     items.push(
@@ -1100,7 +1121,9 @@ impl CompletionsMenu {
                                             ),
                                         );
 
-                                if let Some(lsp::Documentation::String(text)) = documentation {
+                                if let Some(language::Documentation::SingleLine(text)) =
+                                    documentation
+                                {
                                     Flex::row()
                                         .with_child(completion_label)
                                         .with_children((|| {
@@ -1183,39 +1206,18 @@ impl CompletionsMenu {
                 let mat = &self.matches[selected_item];
                 let completions = self.completions.read();
                 let completion = &completions[mat.candidate_id];
-                let documentation = &completion.lsp_completion.documentation;
+                let documentation = &completion.documentation;
 
-                if let Some(lsp::Documentation::MarkupContent(content)) = documentation {
-                    let registry = editor
-                        .project
-                        .as_ref()
-                        .unwrap()
-                        .read(cx)
-                        .languages()
-                        .clone();
-                    let language = self.buffer.read(cx).language().map(Arc::clone);
-
-                    enum CompletionDocsMarkdown {}
-                    Some(
-                        Flex::column()
-                            .scrollable::<CompletionDocsMarkdown>(0, None, cx)
-                            .with_child(render_rendered_markdown(
-                                &language::markdown::render_markdown(
-                                    &content.value,
-                                    &registry,
-                                    &language,
-                                    &style.theme,
-                                ),
-                                &style,
-                                cx,
-                            ))
-                            .constrained()
-                            .with_width(alongside_docs_width)
-                            .contained()
-                            .with_style(alongside_docs_container_style),
-                    )
-                } else {
-                    None
+                match documentation {
+                    Some(language::Documentation::MultiLinePlainText(text)) => {
+                        Some(Text::new(text.clone(), style.text.clone()))
+                    }
+
+                    Some(language::Documentation::MultiLineMarkdown(parsed)) => {
+                        Some(render_parsed_markdown(parsed, &style, cx))
+                    }
+
+                    _ => None,
                 }
             })
             .contained()
@@ -3333,7 +3335,11 @@ impl Editor {
                         None
                     } else {
                         _ = this.update(&mut cx, |editor, cx| {
-                            menu.attempt_resolve_selected_completion(editor.project.as_ref(), cx);
+                            menu.attempt_resolve_selected_completion(
+                                editor.project.as_ref(),
+                                editor.style(cx).theme,
+                                cx,
+                            );
                         });
                         Some(menu)
                     }
@@ -5509,13 +5515,16 @@ impl Editor {
             return;
         }
 
-        if self
-            .context_menu
-            .as_mut()
-            .map(|menu| menu.select_last(self.project.as_ref(), cx))
-            .unwrap_or(false)
-        {
-            return;
+        if self.context_menu.is_some() {
+            let style = self.style(cx).theme;
+            if self
+                .context_menu
+                .as_mut()
+                .map(|menu| menu.select_last(self.project.as_ref(), style, cx))
+                .unwrap_or(false)
+            {
+                return;
+            }
         }
 
         if matches!(self.mode, EditorMode::SingleLine) {
@@ -5555,26 +5564,30 @@ impl Editor {
     }
 
     pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
+        let style = self.style(cx).theme;
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_first(self.project.as_ref(), cx);
+            context_menu.select_first(self.project.as_ref(), style, cx);
         }
     }
 
     pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
+        let style = self.style(cx).theme;
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_prev(self.project.as_ref(), cx);
+            context_menu.select_prev(self.project.as_ref(), style, cx);
         }
     }
 
     pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
+        let style = self.style(cx).theme;
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_next(self.project.as_ref(), cx);
+            context_menu.select_next(self.project.as_ref(), style, cx);
         }
     }
 
     pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
+        let style = self.style(cx).theme;
         if let Some(context_menu) = self.context_menu.as_mut() {
-            context_menu.select_last(self.project.as_ref(), cx);
+            context_menu.select_last(self.project.as_ref(), style, cx);
         }
     }
 

crates/editor/src/hover_popover.rs 🔗

@@ -13,7 +13,7 @@ use gpui::{
     AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
 };
 use language::{
-    markdown::{self, RenderedRegion},
+    markdown::{self, ParsedRegion},
     Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry,
 };
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
@@ -367,9 +367,9 @@ fn render_blocks(
     theme_id: usize,
     blocks: &[HoverBlock],
     language_registry: &Arc<LanguageRegistry>,
-    language: &Option<Arc<Language>>,
+    language: Option<Arc<Language>>,
     style: &EditorStyle,
-) -> RenderedInfo {
+) -> ParsedInfo {
     let mut text = String::new();
     let mut highlights = Vec::new();
     let mut region_ranges = Vec::new();
@@ -382,10 +382,10 @@ fn render_blocks(
                 text.push_str(&block.text);
             }
 
-            HoverBlockKind::Markdown => markdown::render_markdown_block(
+            HoverBlockKind::Markdown => markdown::parse_markdown_block(
                 &block.text,
                 language_registry,
-                language,
+                language.clone(),
                 style,
                 &mut text,
                 &mut highlights,
@@ -399,7 +399,7 @@ fn render_blocks(
                     .now_or_never()
                     .and_then(Result::ok)
                 {
-                    markdown::render_code(
+                    markdown::highlight_code(
                         &mut text,
                         &mut highlights,
                         &block.text,
@@ -413,7 +413,7 @@ fn render_blocks(
         }
     }
 
-    RenderedInfo {
+    ParsedInfo {
         theme_id,
         text: text.trim().to_string(),
         highlights,
@@ -482,16 +482,16 @@ pub struct InfoPopover {
     symbol_range: DocumentRange,
     pub blocks: Vec<HoverBlock>,
     language: Option<Arc<Language>>,
-    rendered_content: Option<RenderedInfo>,
+    rendered_content: Option<ParsedInfo>,
 }
 
 #[derive(Debug, Clone)]
-struct RenderedInfo {
+struct ParsedInfo {
     theme_id: usize,
     text: String,
     highlights: Vec<(Range<usize>, HighlightStyle)>,
     region_ranges: Vec<Range<usize>>,
-    regions: Vec<RenderedRegion>,
+    regions: Vec<ParsedRegion>,
 }
 
 impl InfoPopover {
@@ -511,7 +511,7 @@ impl InfoPopover {
                 style.theme_id,
                 &self.blocks,
                 self.project.read(cx).languages(),
-                &self.language,
+                self.language.clone(),
                 style,
             )
         });
@@ -877,7 +877,7 @@ mod tests {
             );
 
             let style = editor.style(cx);
-            let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style);
+            let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
             assert_eq!(
                 rendered.text,
                 code_str.trim(),
@@ -1069,7 +1069,7 @@ mod tests {
                 expected_styles,
             } in &rows[0..]
             {
-                let rendered = render_blocks(0, &blocks, &Default::default(), &None, &style);
+                let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
 
                 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
                 let expected_highlights = ranges

crates/language/src/buffer.rs 🔗

@@ -1,12 +1,13 @@
 pub use crate::{
     diagnostic_set::DiagnosticSet,
     highlight_map::{HighlightId, HighlightMap},
-    markdown::RenderedMarkdown,
+    markdown::ParsedMarkdown,
     proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
 };
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
     language_settings::{language_settings, LanguageSettings},
+    markdown,
     outline::OutlineItem,
     syntax_map::{
         SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
@@ -144,12 +145,51 @@ pub struct Diagnostic {
     pub is_unnecessary: bool,
 }
 
+pub fn prepare_completion_documentation(
+    documentation: &lsp::Documentation,
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<Arc<Language>>,
+    style: &theme::Editor,
+) -> Option<Documentation> {
+    match documentation {
+        lsp::Documentation::String(text) => {
+            if text.lines().count() <= 1 {
+                Some(Documentation::SingleLine(text.clone()))
+            } else {
+                Some(Documentation::MultiLinePlainText(text.clone()))
+            }
+        }
+
+        lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
+            lsp::MarkupKind::PlainText => {
+                if value.lines().count() <= 1 {
+                    Some(Documentation::SingleLine(value.clone()))
+                } else {
+                    Some(Documentation::MultiLinePlainText(value.clone()))
+                }
+            }
+
+            lsp::MarkupKind::Markdown => {
+                let parsed = markdown::parse_markdown(value, language_registry, language, style);
+                Some(Documentation::MultiLineMarkdown(parsed))
+            }
+        },
+    }
+}
+
+#[derive(Clone, Debug)]
+pub enum Documentation {
+    SingleLine(String),
+    MultiLinePlainText(String),
+    MultiLineMarkdown(ParsedMarkdown),
+}
+
 #[derive(Clone, Debug)]
 pub struct Completion {
     pub old_range: Range<Anchor>,
     pub new_text: String,
     pub label: CodeLabel,
-    pub alongside_documentation: Option<RenderedMarkdown>,
+    pub documentation: Option<Documentation>,
     pub server_id: LanguageServerId,
     pub lsp_completion: lsp::CompletionItem,
 }

crates/language/src/markdown.rs 🔗

@@ -7,31 +7,31 @@ use gpui::fonts::{HighlightStyle, Underline, Weight};
 use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 
 #[derive(Debug, Clone)]
-pub struct RenderedMarkdown {
+pub struct ParsedMarkdown {
     pub text: String,
     pub highlights: Vec<(Range<usize>, HighlightStyle)>,
     pub region_ranges: Vec<Range<usize>>,
-    pub regions: Vec<RenderedRegion>,
+    pub regions: Vec<ParsedRegion>,
 }
 
 #[derive(Debug, Clone)]
-pub struct RenderedRegion {
+pub struct ParsedRegion {
     pub code: bool,
     pub link_url: Option<String>,
 }
 
-pub fn render_markdown(
+pub fn parse_markdown(
     markdown: &str,
     language_registry: &Arc<LanguageRegistry>,
-    language: &Option<Arc<Language>>,
+    language: Option<Arc<Language>>,
     style: &theme::Editor,
-) -> RenderedMarkdown {
+) -> ParsedMarkdown {
     let mut text = String::new();
     let mut highlights = Vec::new();
     let mut region_ranges = Vec::new();
     let mut regions = Vec::new();
 
-    render_markdown_block(
+    parse_markdown_block(
         markdown,
         language_registry,
         language,
@@ -42,7 +42,7 @@ pub fn render_markdown(
         &mut regions,
     );
 
-    RenderedMarkdown {
+    ParsedMarkdown {
         text,
         highlights,
         region_ranges,
@@ -50,15 +50,15 @@ pub fn render_markdown(
     }
 }
 
-pub fn render_markdown_block(
+pub fn parse_markdown_block(
     markdown: &str,
     language_registry: &Arc<LanguageRegistry>,
-    language: &Option<Arc<Language>>,
+    language: Option<Arc<Language>>,
     style: &theme::Editor,
     text: &mut String,
     highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
     region_ranges: &mut Vec<Range<usize>>,
-    regions: &mut Vec<RenderedRegion>,
+    regions: &mut Vec<ParsedRegion>,
 ) {
     let mut bold_depth = 0;
     let mut italic_depth = 0;
@@ -71,7 +71,7 @@ pub fn render_markdown_block(
         match event {
             Event::Text(t) => {
                 if let Some(language) = &current_language {
-                    render_code(text, highlights, t.as_ref(), language, style);
+                    highlight_code(text, highlights, t.as_ref(), language, style);
                 } else {
                     text.push_str(t.as_ref());
 
@@ -84,7 +84,7 @@ pub fn render_markdown_block(
                     }
                     if let Some(link_url) = link_url.clone() {
                         region_ranges.push(prev_len..text.len());
-                        regions.push(RenderedRegion {
+                        regions.push(ParsedRegion {
                             link_url: Some(link_url),
                             code: false,
                         });
@@ -124,7 +124,7 @@ pub fn render_markdown_block(
                         },
                     ));
                 }
-                regions.push(RenderedRegion {
+                regions.push(ParsedRegion {
                     code: true,
                     link_url: link_url.clone(),
                 });
@@ -202,7 +202,7 @@ pub fn render_markdown_block(
     }
 }
 
-pub fn render_code(
+pub fn highlight_code(
     text: &mut String,
     highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
     content: &str,

crates/language/src/proto.rs 🔗

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

crates/project/src/lsp_command.rs 🔗

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