From c7cc07aafb8af32f5916c21b438069f37b4191a1 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 1 Jun 2022 23:05:31 -0700 Subject: [PATCH] working markdown rendering --- 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(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 45177a196e..12c62d5489 100644 --- a/crates/editor/src/editor.rs +++ b/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, - // 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) { 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) { + fn hide_hover(&mut self, cx: &mut ViewContext) { 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) { + fn show_hover(&mut self, mut point: DisplayPoint, cx: &mut ViewContext) { 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 { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fa70384156..85e4e3ccaa 100644 --- a/crates/editor/src/element.rs +++ b/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, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4fefc80c28..10c2983bae 100644 --- a/crates/project/src/lsp_command.rs +++ b/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, mut cx: AsyncAppContext, ) -> Result { - 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) { + 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, + project: &Project, + ) -> Option { + 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 = 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 }) })) }