6080: Add hover config `linksInHover` to suppress links r=flw-cn a=flw-cn

This PR solves the problem of using RA under vim8. It should close #6014.

Since vim8's popup-window doesn't capture focus, the URL given by RA is effectively useless. links are neither displayed correctly nor can they be clicked. This makes the hover window ugly and inefficient.

I'm providing this patch so that people who share my confusion (which I'm almost certain vim8 users do) will have a way to remove links from markdown.

I noticed that [gopls has an option](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#linksinhover-bool) for a similar purpose. So I added an option `linksInHover` to enable this behavior. This is a bool value and defaults to `true` to keep the behavior consistent with the master version. But you can suppress the links in the hover text by setting it to `false`.

The name of my option, `linksInHover`, is borrowed from gopls.

Before applying this patch:

<img width="1280" alt="image" src="https://user-images.githubusercontent.com/5546718/93285021-85698a00-f806-11ea-911d-e77fea4a47f0.png">

After applying this patch(with `"rust-analyzer.hoverActions.linksInHover": false,`):
<img width="1280" alt="image" src="https://user-images.githubusercontent.com/5546718/94332256-2e359780-0006-11eb-9724-1aed14130d0d.png">

This is the full test cases:
```
fn main() {
    let args: Vec<String> = std::env::args().collect();
    test();
    println!("args: {:?}", args);
}

/// Test cases:
/// case 1.  bare URL: https://rust-lang.org/
/// case 2.  inline URL with title: [foo](https://rust-lang.org/)
/// case 3.  code refrence: [`Result`]
/// case 4.  code refrence but miss footnote: [`String`]
/// case 5.  autolink: <http://rust-lang.org/>
/// case 6.  email address: <test@example.com>
/// case 7.  refrence: [bing][google]
/// case 8.  collapsed link: [bing][]
/// case 9.  shortcut link: [bing]
/// case 10. inline without URL: [bing]()
/// case 11. refrence: [foo][foo]
/// case 12. refrence: [foo][bar]
/// case 13. collapsed link: [foo][]
/// case 14. shortcut link: [foo]
/// case 15. inline without URL: [foo]()
/// case 16. just escaped text: \[hello]
/// case 17. inline link: [Foo](foo::Foo)
///
/// [`Result`]: ../../std/result/enum.Result.html
/// [^bing]: https://www.bing.com/
/// [^google]: https://www.google.com/
pub fn test() {
    println!("Hello");
}
```

screenshot:
<img width="1278" alt="image" src="https://user-images.githubusercontent.com/5546718/94332055-45738580-0004-11eb-9153-707f508d0c4b.png">

Co-authored-by: flw <flw@cpan.org>
This commit is contained in:
bors[bot] 2020-09-29 12:10:47 +00:00 committed by GitHub
commit e813de6cdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 149 additions and 13 deletions

View file

@ -14,7 +14,7 @@ use test_utils::mark;
use crate::{
display::{macro_label, ShortLabel, ToNav, TryToNav},
link_rewrite::rewrite_links,
link_rewrite::{remove_links, rewrite_links},
markup::Markup,
runnables::runnable,
FileId, FilePosition, NavigationTarget, RangeInfo, Runnable,
@ -26,17 +26,29 @@ pub struct HoverConfig {
pub run: bool,
pub debug: bool,
pub goto_type_def: bool,
pub links_in_hover: bool,
}
impl Default for HoverConfig {
fn default() -> Self {
Self { implementations: true, run: true, debug: true, goto_type_def: true }
Self {
implementations: true,
run: true,
debug: true,
goto_type_def: true,
links_in_hover: true,
}
}
}
impl HoverConfig {
pub const NO_ACTIONS: Self =
Self { implementations: false, run: false, debug: false, goto_type_def: false };
pub const NO_ACTIONS: Self = Self {
implementations: false,
run: false,
debug: false,
goto_type_def: false,
links_in_hover: true,
};
pub fn any(&self) -> bool {
self.implementations || self.runnable() || self.goto_type_def
@ -75,7 +87,11 @@ pub struct HoverResult {
//
// Shows additional information, like type of an expression or documentation for definition when "focusing" code.
// Focusing is usually hovering with a mouse, but can also be triggered with a shortcut.
pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeInfo<HoverResult>> {
pub(crate) fn hover(
db: &RootDatabase,
position: FilePosition,
links_in_hover: bool,
) -> Option<RangeInfo<HoverResult>> {
let sema = Semantics::new(db);
let file = sema.parse(position.file_id).syntax().clone();
let token = pick_best(file.token_at_offset(position.offset))?;
@ -93,7 +109,11 @@ pub(crate) fn hover(db: &RootDatabase, position: FilePosition) -> Option<RangeIn
};
if let Some(definition) = definition {
if let Some(markup) = hover_for_definition(db, definition) {
let markup = rewrite_links(db, &markup.as_str(), &definition);
let markup = if links_in_hover {
rewrite_links(db, &markup.as_str(), &definition)
} else {
remove_links(&markup.as_str())
};
res.markup = Markup::from(markup);
if let Some(action) = show_implementations_action(db, definition) {
res.actions.push(action);
@ -363,12 +383,23 @@ mod tests {
fn check_hover_no_result(ra_fixture: &str) {
let (analysis, position) = analysis_and_position(ra_fixture);
assert!(analysis.hover(position).unwrap().is_none());
assert!(analysis.hover(position, true).unwrap().is_none());
}
fn check(ra_fixture: &str, expect: Expect) {
let (analysis, position) = analysis_and_position(ra_fixture);
let hover = analysis.hover(position).unwrap().unwrap();
let hover = analysis.hover(position, true).unwrap().unwrap();
let content = analysis.db.file_text(position.file_id);
let hovered_element = &content[hover.range];
let actual = format!("*{}*\n{}\n", hovered_element, hover.info.markup);
expect.assert_eq(&actual)
}
fn check_hover_no_links(ra_fixture: &str, expect: Expect) {
let (analysis, position) = analysis_and_position(ra_fixture);
let hover = analysis.hover(position, false).unwrap().unwrap();
let content = analysis.db.file_text(position.file_id);
let hovered_element = &content[hover.range];
@ -379,7 +410,7 @@ mod tests {
fn check_actions(ra_fixture: &str, expect: Expect) {
let (analysis, position) = analysis_and_position(ra_fixture);
let hover = analysis.hover(position).unwrap().unwrap();
let hover = analysis.hover(position, true).unwrap().unwrap();
expect.assert_debug_eq(&hover.info.actions)
}
@ -1809,6 +1840,70 @@ struct S {
);
}
#[test]
fn test_hover_no_links() {
check_hover_no_links(
r#"
/// Test cases:
/// case 1. bare URL: https://www.example.com/
/// case 2. inline URL with title: [example](https://www.example.com/)
/// case 3. code refrence: [`Result`]
/// case 4. code refrence but miss footnote: [`String`]
/// case 5. autolink: <http://www.example.com/>
/// case 6. email address: <test@example.com>
/// case 7. refrence: [example][example]
/// case 8. collapsed link: [example][]
/// case 9. shortcut link: [example]
/// case 10. inline without URL: [example]()
/// case 11. refrence: [foo][foo]
/// case 12. refrence: [foo][bar]
/// case 13. collapsed link: [foo][]
/// case 14. shortcut link: [foo]
/// case 15. inline without URL: [foo]()
/// case 16. just escaped text: \[foo]
/// case 17. inline link: [Foo](foo::Foo)
///
/// [`Result`]: ../../std/result/enum.Result.html
/// [^example]: https://www.example.com/
pub fn fo<|>o() {}
"#,
expect![[r#"
*foo*
```rust
test
```
```rust
pub fn foo()
```
---
Test cases:
case 1. bare URL: https://www.example.com/
case 2. inline URL with title: [example](https://www.example.com/)
case 3. code refrence: `Result`
case 4. code refrence but miss footnote: `String`
case 5. autolink: http://www.example.com/
case 6. email address: test@example.com
case 7. refrence: example
case 8. collapsed link: example
case 9. shortcut link: example
case 10. inline without URL: example
case 11. refrence: foo
case 12. refrence: foo
case 13. collapsed link: foo
case 14. shortcut link: foo
case 15. inline without URL: foo
case 16. just escaped text: \[foo]
case 17. inline link: Foo
[^example]: https://www.example.com/
"#]],
);
}
#[test]
fn test_hover_macro_generated_struct_fn_doc_comment() {
mark::check!(hover_macro_generated_struct_fn_doc_comment);

View file

@ -370,8 +370,12 @@ impl Analysis {
}
/// Returns a short text describing element at position.
pub fn hover(&self, position: FilePosition) -> Cancelable<Option<RangeInfo<HoverResult>>> {
self.with_db(|db| hover::hover(db, position))
pub fn hover(
&self,
position: FilePosition,
links_in_hover: bool,
) -> Cancelable<Option<RangeInfo<HoverResult>>> {
self.with_db(|db| hover::hover(db, position, links_in_hover))
}
/// Computes parameter information for the given call expression.

View file

@ -4,7 +4,7 @@
use hir::{Adt, Crate, HasAttrs, ModuleDef};
use ide_db::{defs::Definition, RootDatabase};
use pulldown_cmark::{CowStr, Event, Options, Parser, Tag};
use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
use url::Url;
@ -45,6 +45,41 @@ pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition)
out
}
/// Remove all links in markdown documentation.
pub fn remove_links(markdown: &str) -> String {
let mut drop_link = false;
let mut opts = Options::empty();
opts.insert(Options::ENABLE_FOOTNOTES);
let doc = Parser::new_with_broken_link_callback(
markdown,
opts,
Some(&|_, _| Some((String::new(), String::new()))),
);
let doc = doc.filter_map(move |evt| match evt {
Event::Start(Tag::Link(link_type, ref target, ref title)) => {
if link_type == LinkType::Inline && target.contains("://") {
Some(Event::Start(Tag::Link(link_type, target.clone(), title.clone())))
} else {
drop_link = true;
None
}
}
Event::End(_) if drop_link => {
drop_link = false;
None
}
_ => Some(evt),
});
let mut out = String::new();
let mut options = CmarkOptions::default();
options.code_block_backticks = 3;
cmark_with_options(doc, &mut out, None, options).ok();
out
}
fn rewrite_intra_doc_link(
db: &RootDatabase,
def: Definition,

View file

@ -307,6 +307,7 @@ impl Config {
run: data.hoverActions_enable && data.hoverActions_run,
debug: data.hoverActions_enable && data.hoverActions_debug,
goto_type_def: data.hoverActions_enable && data.hoverActions_gotoTypeDef,
links_in_hover: data.hoverActions_linksInHover,
};
log::info!("Config::update() = {:#?}", self);
@ -451,6 +452,7 @@ config_data! {
hoverActions_gotoTypeDef: bool = true,
hoverActions_implementations: bool = true,
hoverActions_run: bool = true,
hoverActions_linksInHover: bool = true,
inlayHints_chainingHints: bool = true,
inlayHints_maxLength: Option<usize> = None,

View file

@ -597,7 +597,7 @@ pub(crate) fn handle_hover(
) -> Result<Option<lsp_ext::Hover>> {
let _p = profile::span("handle_hover");
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
let info = match snap.analysis.hover(position)? {
let info = match snap.analysis.hover(position, snap.config.hover.links_in_hover)? {
None => return Ok(None),
Some(info) => info,
};