working markdown rendering

This commit is contained in:
Keith Simmons 2022-06-01 23:05:31 -07:00
parent efd798f5f6
commit c7cc07aafb
3 changed files with 125 additions and 70 deletions

View file

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

View file

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

View file

@ -8,7 +8,8 @@ use language::{
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16, 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}; use std::{cmp::Reverse, ops::Range, path::Path};
#[async_trait(?Send)] #[async_trait(?Send)]
@ -805,7 +806,7 @@ impl LspCommand for GetHover {
type LspRequest = lsp::request::HoverRequest; type LspRequest = lsp::request::HoverRequest;
type ProtoRequest = proto::GetHover; 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 { lsp::HoverParams {
text_document_position_params: lsp::TextDocumentPositionParams { text_document_position_params: lsp::TextDocumentPositionParams {
text_document: lsp::TextDocumentIdentifier { text_document: lsp::TextDocumentIdentifier {
@ -824,7 +825,7 @@ impl LspCommand for GetHover {
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<Self::Response> { ) -> Result<Self::Response> {
Ok(message.map(|hover| { Ok(message.and_then(|hover| {
let range = hover.range.map(|range| { let range = hover.range.map(|range| {
cx.read(|cx| { cx.read(|cx| {
let buffer = buffer.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 { fn text_and_language(marked_string: MarkedString) -> (String, Option<String>) {
match lsp_marked_string { match marked_string {
lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => { MarkedString::LanguageString(LanguageString { language, value }) => {
if let Some(language) = project.languages().get_language(&language) { (value, Some(language))
let runs =
language.highlight_text(&value.as_str().into(), 0..value.len());
HoverContents { text: value, runs }
} else {
HoverContents {
text: value,
runs: Vec::new(),
}
}
} }
lsp::MarkedString::String(text) => HoverContents { MarkedString::String(text) => (text, None),
text, }
}
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(), runs: Vec::new(),
}, })
} }
} }
let contents = cx.read(|cx| { let contents = cx.read(|cx| {
let project = project.read(cx); let project = project.read(cx);
match dbg!(hover.contents) { match hover.contents {
lsp::HoverContents::Scalar(marked_string) => { 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) => { lsp::HoverContents::Markup(markup_content) => {
// TODO: handle markdown let mut contents = Vec::new();
vec![HoverContents { let mut language = None;
text: markup_content.value, let mut current_text = String::new();
runs: Vec::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 })
})) }))
} }