Fix markdown removal in hover handling whitespace weirdly

This commit is contained in:
Lukas Wirth 2023-01-20 14:29:12 +01:00
parent 7385467f2e
commit c5b1e3f2ae
7 changed files with 202 additions and 64 deletions

View File

@ -19,6 +19,7 @@
use crate::{ use crate::{
doc_links::token_as_doc_comment, doc_links::token_as_doc_comment,
markdown_remove::remove_markdown,
markup::Markup, markup::Markup,
runnables::{runnable_fn, runnable_mod}, runnables::{runnable_fn, runnable_mod},
FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, TryToNav, FileId, FilePosition, NavigationTarget, RangeInfo, Runnable, TryToNav,
@ -26,14 +27,9 @@
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct HoverConfig { pub struct HoverConfig {
pub links_in_hover: bool, pub links_in_hover: bool,
pub documentation: Option<HoverDocFormat>, pub documentation: bool,
pub keywords: bool, pub keywords: bool,
} pub format: HoverDocFormat,
impl HoverConfig {
fn markdown(&self) -> bool {
matches!(self.documentation, Some(HoverDocFormat::Markdown))
}
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -90,12 +86,23 @@ pub struct HoverResult {
// image::https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif[] // image::https://user-images.githubusercontent.com/48062697/113020658-b5f98b80-917a-11eb-9f88-3dbc27320c95.gif[]
pub(crate) fn hover( pub(crate) fn hover(
db: &RootDatabase, db: &RootDatabase,
FileRange { file_id, range }: FileRange, file_range: FileRange,
config: &HoverConfig, config: &HoverConfig,
) -> Option<RangeInfo<HoverResult>> { ) -> Option<RangeInfo<HoverResult>> {
let sema = &hir::Semantics::new(db); let sema = &hir::Semantics::new(db);
let file = sema.parse(file_id).syntax().clone(); let mut res = hover_impl(sema, file_range, config)?;
if let HoverDocFormat::PlainText = config.format {
res.info.markup = remove_markdown(res.info.markup.as_str()).into();
}
Some(res)
}
fn hover_impl(
sema: &Semantics<'_, RootDatabase>,
FileRange { file_id, range }: FileRange,
config: &HoverConfig,
) -> Option<RangeInfo<HoverResult>> {
let file = sema.parse(file_id).syntax().clone();
if !range.is_empty() { if !range.is_empty() {
return hover_ranged(&file, range, sema, config); return hover_ranged(&file, range, sema, config);
} }

View File

@ -26,13 +26,12 @@
use crate::{ use crate::{
doc_links::{remove_links, rewrite_links}, doc_links::{remove_links, rewrite_links},
hover::walk_and_push_ty, hover::walk_and_push_ty,
markdown_remove::remove_markdown,
HoverAction, HoverConfig, HoverResult, Markup, HoverAction, HoverConfig, HoverResult, Markup,
}; };
pub(super) fn type_info( pub(super) fn type_info(
sema: &Semantics<'_, RootDatabase>, sema: &Semantics<'_, RootDatabase>,
config: &HoverConfig, _config: &HoverConfig,
expr_or_pat: &Either<ast::Expr, ast::Pat>, expr_or_pat: &Either<ast::Expr, ast::Pat>,
) -> Option<HoverResult> { ) -> Option<HoverResult> {
let TypeInfo { original, adjusted } = match expr_or_pat { let TypeInfo { original, adjusted } = match expr_or_pat {
@ -55,19 +54,15 @@ pub(super) fn type_info(
let adjusted = adjusted_ty.display(sema.db).to_string(); let adjusted = adjusted_ty.display(sema.db).to_string();
let static_text_diff_len = "Coerced to: ".len() - "Type: ".len(); let static_text_diff_len = "Coerced to: ".len() - "Type: ".len();
format!( format!(
"{bt_start}Type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}", "```text\nType: {:>apad$}\nCoerced to: {:>opad$}\n```\n",
original, original,
adjusted, adjusted,
apad = static_text_diff_len + adjusted.len().max(original.len()), apad = static_text_diff_len + adjusted.len().max(original.len()),
opad = original.len(), opad = original.len(),
bt_start = if config.markdown() { "```text\n" } else { "" },
bt_end = if config.markdown() { "```\n" } else { "" }
) )
.into() .into()
} else if config.markdown() {
Markup::fenced_block(&original.display(sema.db))
} else { } else {
original.display(sema.db).to_string().into() Markup::fenced_block(&original.display(sema.db))
}; };
res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets)); res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
Some(res) Some(res)
@ -75,7 +70,7 @@ pub(super) fn type_info(
pub(super) fn try_expr( pub(super) fn try_expr(
sema: &Semantics<'_, RootDatabase>, sema: &Semantics<'_, RootDatabase>,
config: &HoverConfig, _config: &HoverConfig,
try_expr: &ast::TryExpr, try_expr: &ast::TryExpr,
) -> Option<HoverResult> { ) -> Option<HoverResult> {
let inner_ty = sema.type_of_expr(&try_expr.expr()?)?.original; let inner_ty = sema.type_of_expr(&try_expr.expr()?)?.original;
@ -151,14 +146,12 @@ pub(super) fn try_expr(
let ppad = static_text_len_diff.min(0).abs() as usize; let ppad = static_text_len_diff.min(0).abs() as usize;
res.markup = format!( res.markup = format!(
"{bt_start}{} Type: {:>pad0$}\nPropagated as: {:>pad1$}\n{bt_end}", "```text\n{} Type: {:>pad0$}\nPropagated as: {:>pad1$}\n```\n",
s, s,
inner_ty, inner_ty,
body_ty, body_ty,
pad0 = ty_len_max + tpad, pad0 = ty_len_max + tpad,
pad1 = ty_len_max + ppad, pad1 = ty_len_max + ppad,
bt_start = if config.markdown() { "```text\n" } else { "" },
bt_end = if config.markdown() { "```\n" } else { "" }
) )
.into(); .into();
Some(res) Some(res)
@ -166,7 +159,7 @@ pub(super) fn try_expr(
pub(super) fn deref_expr( pub(super) fn deref_expr(
sema: &Semantics<'_, RootDatabase>, sema: &Semantics<'_, RootDatabase>,
config: &HoverConfig, _config: &HoverConfig,
deref_expr: &ast::PrefixExpr, deref_expr: &ast::PrefixExpr,
) -> Option<HoverResult> { ) -> Option<HoverResult> {
let inner_ty = sema.type_of_expr(&deref_expr.expr()?)?.original; let inner_ty = sema.type_of_expr(&deref_expr.expr()?)?.original;
@ -195,15 +188,13 @@ pub(super) fn deref_expr(
.max(adjusted.len() + coerced_len) .max(adjusted.len() + coerced_len)
.max(inner.len() + deref_len); .max(inner.len() + deref_len);
format!( format!(
"{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\nCoerced to: {:>opad$}\n{bt_end}", "```text\nDereferenced from: {:>ipad$}\nTo type: {:>apad$}\nCoerced to: {:>opad$}\n```\n",
inner, inner,
original, original,
adjusted, adjusted,
ipad = max_len - deref_len, ipad = max_len - deref_len,
apad = max_len - type_len, apad = max_len - type_len,
opad = max_len - coerced_len, opad = max_len - coerced_len,
bt_start = if config.markdown() { "```text\n" } else { "" },
bt_end = if config.markdown() { "```\n" } else { "" }
) )
.into() .into()
} else { } else {
@ -213,13 +204,11 @@ pub(super) fn deref_expr(
let deref_len = "Dereferenced from: ".len(); let deref_len = "Dereferenced from: ".len();
let max_len = (original.len() + type_len).max(inner.len() + deref_len); let max_len = (original.len() + type_len).max(inner.len() + deref_len);
format!( format!(
"{bt_start}Dereferenced from: {:>ipad$}\nTo type: {:>apad$}\n{bt_end}", "```text\nDereferenced from: {:>ipad$}\nTo type: {:>apad$}\n```\n",
inner, inner,
original, original,
ipad = max_len - deref_len, ipad = max_len - deref_len,
apad = max_len - type_len, apad = max_len - type_len,
bt_start = if config.markdown() { "```text\n" } else { "" },
bt_end = if config.markdown() { "```\n" } else { "" }
) )
.into() .into()
}; };
@ -233,7 +222,7 @@ pub(super) fn keyword(
config: &HoverConfig, config: &HoverConfig,
token: &SyntaxToken, token: &SyntaxToken,
) -> Option<HoverResult> { ) -> Option<HoverResult> {
if !token.kind().is_keyword() || !config.documentation.is_some() || !config.keywords { if !token.kind().is_keyword() || !config.documentation || !config.keywords {
return None; return None;
} }
let parent = token.parent()?; let parent = token.parent()?;
@ -257,7 +246,7 @@ pub(super) fn keyword(
/// i.e. `let S {a, ..} = S {a: 1, b: 2}` /// i.e. `let S {a, ..} = S {a: 1, b: 2}`
pub(super) fn struct_rest_pat( pub(super) fn struct_rest_pat(
sema: &Semantics<'_, RootDatabase>, sema: &Semantics<'_, RootDatabase>,
config: &HoverConfig, _config: &HoverConfig,
pattern: &RecordPat, pattern: &RecordPat,
) -> HoverResult { ) -> HoverResult {
let missing_fields = sema.record_pattern_missing_fields(pattern); let missing_fields = sema.record_pattern_missing_fields(pattern);
@ -286,11 +275,7 @@ pub(super) fn struct_rest_pat(
// get rid of trailing comma // get rid of trailing comma
s.truncate(s.len() - 2); s.truncate(s.len() - 2);
if config.markdown() { Markup::fenced_block(&s)
Markup::fenced_block(&s)
} else {
s.into()
}
}; };
res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets)); res.actions.push(HoverAction::goto_type_from_targets(sema.db, targets));
res res
@ -344,13 +329,8 @@ pub(super) fn process_markup(
config: &HoverConfig, config: &HoverConfig,
) -> Markup { ) -> Markup {
let markup = markup.as_str(); let markup = markup.as_str();
let markup = if !config.markdown() { let markup =
remove_markdown(markup) if config.links_in_hover { rewrite_links(db, markup, def) } else { remove_links(markup) };
} else if config.links_in_hover {
rewrite_links(db, markup, def)
} else {
remove_links(markup)
};
Markup::from(markup) Markup::from(markup)
} }
@ -463,8 +443,9 @@ pub(super) fn definition(
Definition::DeriveHelper(it) => (format!("derive_helper {}", it.name(db)), None), Definition::DeriveHelper(it) => (format!("derive_helper {}", it.name(db)), None),
}; };
let docs = match config.documentation { let docs = docs
Some(_) => docs.or_else(|| { .filter(|_| config.documentation)
.or_else(|| {
// docs are missing, for assoc items of trait impls try to fall back to the docs of the // docs are missing, for assoc items of trait impls try to fall back to the docs of the
// original item of the trait // original item of the trait
let assoc = def.as_assoc_item(db)?; let assoc = def.as_assoc_item(db)?;
@ -472,10 +453,8 @@ pub(super) fn definition(
let name = Some(assoc.name(db)?); let name = Some(assoc.name(db)?);
let item = trait_.items(db).into_iter().find(|it| it.name(db) == name)?; let item = trait_.items(db).into_iter().find(|it| it.name(db) == name)?;
item.docs(db) item.docs(db)
}), })
None => None, .map(Into::into);
};
let docs = docs.filter(|_| config.documentation.is_some()).map(Into::into);
markup(docs, label, mod_path) markup(docs, label, mod_path)
} }

View File

@ -2,7 +2,7 @@
use ide_db::base_db::{FileLoader, FileRange}; use ide_db::base_db::{FileLoader, FileRange};
use syntax::TextRange; use syntax::TextRange;
use crate::{fixture, hover::HoverDocFormat, HoverConfig}; use crate::{fixture, HoverConfig, HoverDocFormat};
fn check_hover_no_result(ra_fixture: &str) { fn check_hover_no_result(ra_fixture: &str) {
let (analysis, position) = fixture::position(ra_fixture); let (analysis, position) = fixture::position(ra_fixture);
@ -10,8 +10,9 @@ fn check_hover_no_result(ra_fixture: &str) {
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: true,
keywords: true, keywords: true,
format: HoverDocFormat::Markdown,
}, },
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
@ -26,8 +27,9 @@ fn check(ra_fixture: &str, expect: Expect) {
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: true,
keywords: true, keywords: true,
format: HoverDocFormat::Markdown,
}, },
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
@ -47,8 +49,9 @@ fn check_hover_no_links(ra_fixture: &str, expect: Expect) {
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: false, links_in_hover: false,
documentation: Some(HoverDocFormat::Markdown), documentation: true,
keywords: true, keywords: true,
format: HoverDocFormat::Markdown,
}, },
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
@ -68,8 +71,9 @@ fn check_hover_no_markdown(ra_fixture: &str, expect: Expect) {
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::PlainText), documentation: true,
keywords: true, keywords: true,
format: HoverDocFormat::PlainText,
}, },
FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) }, FileRange { file_id: position.file_id, range: TextRange::empty(position.offset) },
) )
@ -89,8 +93,9 @@ fn check_actions(ra_fixture: &str, expect: Expect) {
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: true,
keywords: true, keywords: true,
format: HoverDocFormat::Markdown,
}, },
FileRange { file_id, range: position.range_or_empty() }, FileRange { file_id, range: position.range_or_empty() },
) )
@ -105,8 +110,9 @@ fn check_hover_range(ra_fixture: &str, expect: Expect) {
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: false, links_in_hover: false,
documentation: Some(HoverDocFormat::Markdown), documentation: true,
keywords: true, keywords: true,
format: HoverDocFormat::Markdown,
}, },
range, range,
) )
@ -121,8 +127,9 @@ fn check_hover_range_no_results(ra_fixture: &str) {
.hover( .hover(
&HoverConfig { &HoverConfig {
links_in_hover: false, links_in_hover: false,
documentation: Some(HoverDocFormat::Markdown), documentation: true,
keywords: true, keywords: true,
format: HoverDocFormat::Markdown,
}, },
range, range,
) )

View File

@ -14,9 +14,154 @@ pub(crate) fn remove_markdown(markdown: &str) -> String {
Event::SoftBreak | Event::HardBreak | Event::Rule | Event::End(Tag::CodeBlock(_)) => { Event::SoftBreak | Event::HardBreak | Event::Rule | Event::End(Tag::CodeBlock(_)) => {
out.push('\n') out.push('\n')
} }
_ => {} Event::End(Tag::Paragraph) => {
out.push('\n');
out.push('\n');
}
Event::Start(_)
| Event::End(_)
| Event::Html(_)
| Event::FootnoteReference(_)
| Event::TaskListMarker(_) => (),
} }
} }
if let Some(p) = out.rfind(|c| c != '\n') {
out.drain(p + 1..);
}
out out
} }
#[cfg(test)]
mod tests {
use expect_test::expect;
use super::*;
#[test]
fn smoke_test() {
let res = remove_markdown(
r##"
A function or function pointer.
Functions are the primary way code is executed within Rust. Function blocks, usually just
called functions, can be defined in a variety of different places and be assigned many
different attributes and modifiers.
Standalone functions that just sit within a module not attached to anything else are common,
but most functions will end up being inside [`impl`] blocks, either on another type itself, or
as a trait impl for that type.
```rust
fn standalone_function() {
// code
}
pub fn public_thing(argument: bool) -> String {
// code
# "".to_string()
}
struct Thing {
foo: i32,
}
impl Thing {
pub fn new() -> Self {
Self {
foo: 42,
}
}
}
```
In addition to presenting fixed types in the form of `fn name(arg: type, ..) -> return_type`,
functions can also declare a list of type parameters along with trait bounds that they fall
into.
```rust
fn generic_function<T: Clone>(x: T) -> (T, T, T) {
(x.clone(), x.clone(), x.clone())
}
fn generic_where<T>(x: T) -> T
where T: std::ops::Add<Output = T> + Copy
{
x + x + x
}
```
Declaring trait bounds in the angle brackets is functionally identical to using a `where`
clause. It's up to the programmer to decide which works better in each situation, but `where`
tends to be better when things get longer than one line.
Along with being made public via `pub`, `fn` can also have an [`extern`] added for use in
FFI.
For more information on the various types of functions and how they're used, consult the [Rust
book] or the [Reference].
[`impl`]: keyword.impl.html
[`extern`]: keyword.extern.html
[Rust book]: ../book/ch03-03-how-functions-work.html
[Reference]: ../reference/items/functions.html
"##,
);
expect![[r#"
A function or function pointer.
Functions are the primary way code is executed within Rust. Function blocks, usually just
called functions, can be defined in a variety of different places and be assigned many
different attributes and modifiers.
Standalone functions that just sit within a module not attached to anything else are common,
but most functions will end up being inside impl blocks, either on another type itself, or
as a trait impl for that type.
fn standalone_function() {
// code
}
pub fn public_thing(argument: bool) -> String {
// code
# "".to_string()
}
struct Thing {
foo: i32,
}
impl Thing {
pub fn new() -> Self {
Self {
foo: 42,
}
}
}
In addition to presenting fixed types in the form of fn name(arg: type, ..) -> return_type,
functions can also declare a list of type parameters along with trait bounds that they fall
into.
fn generic_function<T: Clone>(x: T) -> (T, T, T) {
(x.clone(), x.clone(), x.clone())
}
fn generic_where<T>(x: T) -> T
where T: std::ops::Add<Output = T> + Copy
{
x + x + x
}
Declaring trait bounds in the angle brackets is functionally identical to using a where
clause. It's up to the programmer to decide which works better in each situation, but where
tends to be better when things get longer than one line.
Along with being made public via pub, fn can also have an extern added for use in
FFI.
For more information on the various types of functions and how they're used, consult the Rust
book or the Reference."#]].assert_eq(&res);
}
}

View File

@ -16,8 +16,7 @@
inlay_hints::AdjustmentHintsMode, inlay_hints::AdjustmentHintsMode,
moniker::{def_to_moniker, MonikerResult}, moniker::{def_to_moniker, MonikerResult},
parent_module::crates_for, parent_module::crates_for,
Analysis, Fold, HoverConfig, HoverDocFormat, HoverResult, InlayHint, InlayHintsConfig, Analysis, Fold, HoverConfig, HoverResult, InlayHint, InlayHintsConfig, TryToNav,
TryToNav,
}; };
/// A static representation of fully analyzed source code. /// A static representation of fully analyzed source code.
@ -137,8 +136,9 @@ fn add_file(&mut self, file_id: FileId) {
}); });
let hover_config = HoverConfig { let hover_config = HoverConfig {
links_in_hover: true, links_in_hover: true,
documentation: Some(HoverDocFormat::Markdown), documentation: true,
keywords: true, keywords: true,
format: crate::HoverDocFormat::Markdown,
}; };
let tokens = tokens.filter(|token| { let tokens = tokens.filter(|token| {
matches!( matches!(

View File

@ -1393,7 +1393,8 @@ pub fn highlighting_config(&self) -> HighlightConfig {
pub fn hover(&self) -> HoverConfig { pub fn hover(&self) -> HoverConfig {
HoverConfig { HoverConfig {
links_in_hover: self.data.hover_links_enable, links_in_hover: self.data.hover_links_enable,
documentation: self.data.hover_documentation_enable.then(|| { documentation: self.data.hover_documentation_enable,
format: {
let is_markdown = try_or_def!(self let is_markdown = try_or_def!(self
.caps .caps
.text_document .text_document
@ -1409,7 +1410,7 @@ pub fn hover(&self) -> HoverConfig {
} else { } else {
HoverDocFormat::PlainText HoverDocFormat::PlainText
} }
}), },
keywords: self.data.hover_documentation_keywords_enable, keywords: self.data.hover_documentation_keywords_enable,
} }
} }

View File

@ -936,8 +936,7 @@ pub(crate) fn handle_hover(
let line_index = snap.file_line_index(file_range.file_id)?; let line_index = snap.file_line_index(file_range.file_id)?;
let range = to_proto::range(&line_index, info.range); let range = to_proto::range(&line_index, info.range);
let markup_kind = let markup_kind = snap.config.hover().format;
snap.config.hover().documentation.map_or(ide::HoverDocFormat::Markdown, |kind| kind);
let hover = lsp_ext::Hover { let hover = lsp_ext::Hover {
hover: lsp_types::Hover { hover: lsp_types::Hover {
contents: HoverContents::Markup(to_proto::markup_content( contents: HoverContents::Markup(to_proto::markup_content(