working markdown rendering

Keith Simmons created

Change summary

crates/editor/src/editor.rs       |  52 ++++--------
crates/editor/src/element.rs      |  12 +-
crates/project/src/lsp_command.rs | 131 +++++++++++++++++++++++++-------
3 files changed, 125 insertions(+), 70 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -25,7 +25,7 @@ use gpui::{
     geometry::vector::{vec2f, Vector2F},
     impl_actions, impl_internal_actions,
     platform::CursorStyle,
-    text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
+    text_layout, AppContext, AsyncAppContext, Axis, ClipboardItem, Element, ElementBox, Entity,
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
@@ -80,18 +80,9 @@ pub struct Scroll(pub Vector2F);
 #[derive(Clone, PartialEq)]
 pub struct Select(pub SelectPhase);
 
-#[derive(Clone)]
-pub struct ShowHover(DisplayPoint);
-
-#[derive(Clone)]
-pub struct HideHover;
-
 #[derive(Clone)]
 pub struct Hover {
     point: Option<DisplayPoint>,
-    // visible: bool,
-    // TODO(isaac): remove overshoot
-    // overshoot: DisplayPoint,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -223,7 +214,7 @@ impl_actions!(
     ]
 );
 
-impl_internal_actions!(editor, [Scroll, Select, Hover, ShowHover, HideHover, GoToDefinitionAt]);
+impl_internal_actions!(editor, [Scroll, Select, Hover, GoToDefinitionAt]);
 
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
@@ -311,8 +302,6 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::show_completions);
     cx.add_action(Editor::toggle_code_actions);
     cx.add_action(Editor::hover);
-    cx.add_action(Editor::show_hover);
-    cx.add_action(Editor::hide_hover);
     cx.add_action(Editor::open_excerpts);
     cx.add_action(Editor::restart_language_server);
     cx.add_async_action(Editor::confirm_completion);
@@ -454,14 +443,14 @@ impl HoverState {
     /// and returns a tuple containing whether there was a recent hover,
     /// and whether the hover is still in the grace period.
     pub fn determine_state(&mut self, hovering: bool) -> (bool, bool) {
-        // NOTE: 200ms and 100ms are sane defaults, but it might be
+        // NOTE: We use some sane defaults, but it might be
         //       nice to make these values configurable.
-        let recent_hover = self.last_hover.elapsed() < std::time::Duration::from_millis(200);
+        let recent_hover = self.last_hover.elapsed() < std::time::Duration::from_millis(500);
         if !hovering {
             self.last_hover = std::time::Instant::now();
         }
 
-        let in_grace = self.start_grace.elapsed() < std::time::Duration::from_millis(100);
+        let in_grace = self.start_grace.elapsed() < std::time::Duration::from_millis(250);
         if hovering && !recent_hover {
             self.start_grace = std::time::Instant::now();
         }
@@ -902,13 +891,12 @@ struct HoverPopover {
 
 impl HoverPopover {
     fn render(&self, style: EditorStyle) -> (DisplayPoint, ElementBox) {
-        let contents = self.contents.first().unwrap();
-        (
-            self.point,
-            Text::new(contents.text.clone(), style.text.clone())
-                .with_soft_wrap(false)
+        let mut flex = Flex::new(Axis::Vertical);
+        flex.extend(self.contents.iter().map(|content| {
+            Text::new(content.text.clone(), style.text.clone())
+                .with_soft_wrap(true)
                 .with_highlights(
-                    contents
+                    content
                         .runs
                         .iter()
                         .filter_map(|(range, id)| {
@@ -917,9 +905,11 @@ impl HoverPopover {
                         })
                         .collect(),
                 )
-                .contained()
-                .with_style(style.hover_popover)
-                .boxed(),
+                .boxed()
+        }));
+        (
+            self.point,
+            flex.contained().with_style(style.hover_popover).boxed(),
         )
     }
 }
@@ -2473,15 +2463,15 @@ impl Editor {
     /// depending on whether a point to hover over is provided.
     fn hover(&mut self, action: &Hover, cx: &mut ViewContext<Self>) {
         if let Some(point) = action.point {
-            self.show_hover(&ShowHover(point), cx);
+            self.show_hover(point, cx);
         } else {
-            self.hide_hover(&HideHover, cx);
+            self.hide_hover(cx);
         }
     }
 
     /// Hides the type information popup ASAP.
     /// Triggered by the `Hover` action when the cursor is not over a symbol.
-    fn hide_hover(&mut self, _: &HideHover, cx: &mut ViewContext<Self>) {
+    fn hide_hover(&mut self, cx: &mut ViewContext<Self>) {
         let task = cx.spawn_weak(|this, mut cx| {
             async move {
                 if let Some(this) = this.upgrade(&cx) {
@@ -2507,7 +2497,7 @@ impl Editor {
     /// Queries the LSP and shows type info and documentation
     /// about the symbol the mouse is currently hovering over.
     /// Triggered by the `Hover` action when the cursor may be over a symbol.
-    fn show_hover(&mut self, action: &ShowHover, cx: &mut ViewContext<Self>) {
+    fn show_hover(&mut self, mut point: DisplayPoint, cx: &mut ViewContext<Self>) {
         if self.pending_rename.is_some() {
             return;
         }
@@ -2518,9 +2508,6 @@ impl Editor {
             return;
         };
 
-        // we use the mouse cursor position by default
-        let mut point = action.0.clone();
-
         let snapshot = self.snapshot(cx);
         let (buffer, buffer_position) = if let Some(output) = self
             .buffer
@@ -2541,7 +2528,6 @@ impl Editor {
 
         let task = cx.spawn_weak(|this, mut cx| {
             async move {
-                // TODO: what to show while LSP is loading?
                 let mut contents = None;
 
                 let hover = match hover.await {

crates/editor/src/element.rs 🔗

@@ -33,7 +33,7 @@ use std::{
     cmp::{self, Ordering},
     fmt::Write,
     iter,
-    ops::{Not, Range},
+    ops::Range,
 };
 
 struct SelectionLayout {
@@ -1118,8 +1118,8 @@ impl Element for EditorElement {
                 .head()
                 .to_display_point(&snapshot);
 
+            let style = view.style(cx);
             if (start_row..end_row).contains(&newest_selection_head.row()) {
-                let style = view.style(cx);
                 if view.context_menu_visible() {
                     context_menu = view.render_context_menu(newest_selection_head, style.clone());
                 }
@@ -1127,9 +1127,9 @@ impl Element for EditorElement {
                 code_actions_indicator = view
                     .render_code_actions_indicator(&style, cx)
                     .map(|indicator| (newest_selection_head.row(), indicator));
-
-                hover = view.render_hover_popover(style);
             }
+
+            hover = view.render_hover_popover(style);
         });
 
         if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1157,8 +1157,8 @@ impl Element for EditorElement {
                 SizeConstraint {
                     min: Vector2F::zero(),
                     max: vec2f(
-                        f32::INFINITY,
-                        (12. * line_height).min((size.y() - line_height) / 2.),
+                        (120. * em_width).min(size.x()),
+                        (size.y() - line_height) * 3. / 2.,
                     ),
                 },
                 cx,

crates/project/src/lsp_command.rs 🔗

@@ -8,7 +8,8 @@ use language::{
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
     range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16,
 };
-use lsp::{DocumentHighlightKind, ServerCapabilities};
+use lsp::{DocumentHighlightKind, LanguageString, MarkedString, ServerCapabilities};
+use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 use std::{cmp::Reverse, ops::Range, path::Path};
 
 #[async_trait(?Send)]
@@ -805,7 +806,7 @@ impl LspCommand for GetHover {
     type LspRequest = lsp::request::HoverRequest;
     type ProtoRequest = proto::GetHover;
 
-    fn to_lsp(&self, path: &Path, cx: &AppContext) -> lsp::HoverParams {
+    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::HoverParams {
         lsp::HoverParams {
             text_document_position_params: lsp::TextDocumentPositionParams {
                 text_document: lsp::TextDocumentIdentifier {
@@ -824,7 +825,7 @@ impl LspCommand for GetHover {
         buffer: ModelHandle<Buffer>,
         mut cx: AsyncAppContext,
     ) -> Result<Self::Response> {
-        Ok(message.map(|hover| {
+        Ok(message.and_then(|hover| {
             let range = hover.range.map(|range| {
                 cx.read(|cx| {
                     let buffer = buffer.read(cx);
@@ -835,48 +836,116 @@ impl LspCommand for GetHover {
                 })
             });
 
-            fn highlight(lsp_marked_string: lsp::MarkedString, project: &Project) -> HoverContents {
-                match lsp_marked_string {
-                    lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => {
-                        if let Some(language) = project.languages().get_language(&language) {
-                            let runs =
-                                language.highlight_text(&value.as_str().into(), 0..value.len());
-                            HoverContents { text: value, runs }
-                        } else {
-                            HoverContents {
-                                text: value,
-                                runs: Vec::new(),
-                            }
-                        }
+            fn text_and_language(marked_string: MarkedString) -> (String, Option<String>) {
+                match marked_string {
+                    MarkedString::LanguageString(LanguageString { language, value }) => {
+                        (value, Some(language))
                     }
-                    lsp::MarkedString::String(text) => HoverContents {
-                        text,
+                    MarkedString::String(text) => (text, None),
+                }
+            }
+
+            fn highlight(
+                text: String,
+                language: Option<String>,
+                project: &Project,
+            ) -> Option<HoverContents> {
+                let text = text.trim();
+                if text.is_empty() {
+                    return None;
+                }
+
+                if let Some(language) =
+                    language.and_then(|language| project.languages().get_language(&language))
+                {
+                    let runs = language.highlight_text(&text.into(), 0..text.len());
+                    Some(HoverContents {
+                        text: text.to_string(),
+                        runs,
+                    })
+                } else {
+                    Some(HoverContents {
+                        text: text.to_string(),
                         runs: Vec::new(),
-                    },
+                    })
                 }
             }
 
             let contents = cx.read(|cx| {
                 let project = project.read(cx);
-                match dbg!(hover.contents) {
+                match hover.contents {
                     lsp::HoverContents::Scalar(marked_string) => {
-                        vec![highlight(marked_string, project)]
+                        let (text, language) = text_and_language(marked_string);
+                        highlight(text, language, project).map(|content| vec![content])
+                    }
+                    lsp::HoverContents::Array(marked_strings) => {
+                        let content: Vec<HoverContents> = marked_strings
+                            .into_iter()
+                            .filter_map(|marked_string| {
+                                let (text, language) = text_and_language(marked_string);
+                                highlight(text, language, project)
+                            })
+                            .collect();
+                        if content.is_empty() {
+                            None
+                        } else {
+                            Some(content)
+                        }
                     }
-                    lsp::HoverContents::Array(marked_strings) => marked_strings
-                        .into_iter()
-                        .map(|marked_string| highlight(marked_string, project))
-                        .collect(),
                     lsp::HoverContents::Markup(markup_content) => {
-                        // TODO: handle markdown
-                        vec![HoverContents {
-                            text: markup_content.value,
-                            runs: Vec::new(),
-                        }]
+                        let mut contents = Vec::new();
+                        let mut language = None;
+                        let mut current_text = String::new();
+                        for event in Parser::new_ext(&markup_content.value, Options::all()) {
+                            match event {
+                                Event::Text(text) | Event::Code(text) => {
+                                    current_text.push_str(&text.to_string());
+                                }
+                                Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(
+                                    new_language,
+                                ))) => {
+                                    if let Some(content) =
+                                        highlight(current_text.clone(), language, project)
+                                    {
+                                        contents.push(content);
+                                        current_text.clear();
+                                    }
+
+                                    language = if new_language.is_empty() {
+                                        None
+                                    } else {
+                                        Some(new_language.to_string())
+                                    };
+                                }
+                                Event::End(Tag::CodeBlock(_)) => {
+                                    if let Some(content) =
+                                        highlight(current_text.clone(), language.clone(), project)
+                                    {
+                                        contents.push(content);
+                                        current_text.clear();
+                                        language = None;
+                                    }
+                                }
+                                _ => {}
+                            }
+                        }
+
+                        if let Some(content) =
+                            highlight(current_text.clone(), language.clone(), project)
+                        {
+                            contents.push(content);
+                        }
+
+                        if contents.is_empty() {
+                            None
+                        } else {
+                            Some(contents)
+                        }
                     }
                 }
             });
 
-            Hover { contents, range }
+            contents.map(|contents| Hover { contents, range })
         }))
     }