about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-09-29 12:10:47 +0000
committerGitHub <noreply@github.com>2020-09-29 12:10:47 +0000
commite813de6cdd53e542bce8d4a554288dc2f17bbf5e (patch)
tree633e533ce5aa3c10337bafc240e1f4911e4ecb65
parent18c62c8a39d95ce3bb10ff5446bb589b1128a090 (diff)
parente73ee9dfa28e2c093cc79e0e8d729945c43f3c81 (diff)
Merge #6080
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>
-rw-r--r--crates/ide/src/hover.rs113
-rw-r--r--crates/ide/src/lib.rs8
-rw-r--r--crates/ide/src/link_rewrite.rs37
-rw-r--r--crates/rust-analyzer/src/config.rs2
-rw-r--r--crates/rust-analyzer/src/handlers.rs2
5 files changed, 149 insertions, 13 deletions
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 37171cbef33..bb9f12cd37e 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -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)
     }
 
@@ -1810,6 +1841,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);
 
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 3b97e087f88..4763c0aac3a 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -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.
diff --git a/crates/ide/src/link_rewrite.rs b/crates/ide/src/link_rewrite.rs
index acedea71bd5..107787bb91b 100644
--- a/crates/ide/src/link_rewrite.rs
+++ b/crates/ide/src/link_rewrite.rs
@@ -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,
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index 69d05aed5ac..fab15f86009 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -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,
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index c0943a54dc6..f7c7a378a14 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -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,
     };