Repository: rust-analyzer/rust-analyzer
Pull request #5917 (2020-08-30 08:06:27)

Add a command to open docs for the symbol under the cursor
#### Todo

- [ ] Decide if there should be a default keybind or context menu entry
- [x] Figure out how to get the documentation path for methods and other non-top-level defs
- [x] Design the protocol extension. In future we'll probably want parameters for local/remote documentation URLs, so that should maybe be done in this PR?
- [x] Code organisation
- [x] Tests


matklad: Status: `MethodOwner` enum should be removed from hir, and the "let's consider only adt's" logic should be moved to ide layer. Other than that, LGTM!

Patch:
diff --git a/crates/hir/src/code_model.rs b/crates/hir/src/code_model.rs
index 031c91ccf6..650b4fa40c 100644
--- a/crates/hir/src/code_model.rs
+++ b/crates/hir/src/code_model.rs
@@ -186,6 +186,16 @@ impl_from!(
     for ModuleDef
 );
 
+impl From<VariantDef> for ModuleDef {
+    fn from(var: VariantDef) -> Self {
+        match var {
+            VariantDef::Struct(t) => Adt::from(t).into(),
+            VariantDef::Union(t) => Adt::from(t).into(),
+            VariantDef::EnumVariant(t) => t.into(),
+        }
+    }
+}
+
 impl ModuleDef {
     pub fn module(self, db: &dyn HirDatabase) -> Option<Module> {
         match self {
@@ -752,6 +762,13 @@ impl Function {
     pub fn diagnostics(self, db: &dyn HirDatabase, sink: &mut DiagnosticSink) {
         hir_ty::diagnostics::validate_body(db, self.id.into(), sink)
     }
+
+    /// Whether this function declaration has a definition.
+    ///
+    /// This is false in the case of required (not provided) trait methods.
+    pub fn has_body(self, db: &dyn HirDatabase) -> bool {
+        db.function_data(self.id).has_body
+    }
 }
 
 // Note: logically, this belongs to `hir_ty`, but we are not using it there yet.
diff --git a/crates/hir_def/src/data.rs b/crates/hir_def/src/data.rs
index 6190906da9..ff1ef0df64 100644
--- a/crates/hir_def/src/data.rs
+++ b/crates/hir_def/src/data.rs
@@ -25,6 +25,7 @@ pub struct FunctionData {
     /// True if the first param is `self`. This is relevant to decide whether this
     /// can be called as a method.
     pub has_self_param: bool,
+    pub has_body: bool,
     pub is_unsafe: bool,
     pub is_varargs: bool,
     pub visibility: RawVisibility,
@@ -42,6 +43,7 @@ impl FunctionData {
             ret_type: func.ret_type.clone(),
             attrs: item_tree.attrs(ModItem::from(loc.id.value).into()).clone(),
             has_self_param: func.has_self_param,
+            has_body: func.has_body,
             is_unsafe: func.is_unsafe,
             is_varargs: func.is_varargs,
             visibility: item_tree[func.visibility].clone(),
diff --git a/crates/hir_def/src/item_tree.rs b/crates/hir_def/src/item_tree.rs
index 0fd91b9d01..8a1121bbdf 100644
--- a/crates/hir_def/src/item_tree.rs
+++ b/crates/hir_def/src/item_tree.rs
@@ -505,6 +505,7 @@ pub struct Function {
     pub visibility: RawVisibilityId,
     pub generic_params: GenericParamsId,
     pub has_self_param: bool,
+    pub has_body: bool,
     pub is_unsafe: bool,
     pub params: Box<[TypeRef]>,
     pub is_varargs: bool,
diff --git a/crates/hir_def/src/item_tree/lower.rs b/crates/hir_def/src/item_tree/lower.rs
index 54814f1414..3328639cfe 100644
--- a/crates/hir_def/src/item_tree/lower.rs
+++ b/crates/hir_def/src/item_tree/lower.rs
@@ -330,12 +330,15 @@ impl Ctx {
             ret_type
         };
 
+        let has_body = func.body().is_some();
+
         let ast_id = self.source_ast_id_map.ast_id(func);
         let mut res = Function {
             name,
             visibility,
             generic_params: GenericParamsId::EMPTY,
             has_self_param,
+            has_body,
             is_unsafe: func.unsafe_token().is_some(),
             params: params.into_boxed_slice(),
             is_varargs,
diff --git a/crates/ide/src/link_rewrite.rs b/crates/ide/src/doc_links.rs
similarity index 54%
rename from crates/ide/src/link_rewrite.rs
rename to crates/ide/src/doc_links.rs
index c317a2379b..06af36b73e 100644
--- a/crates/ide/src/link_rewrite.rs
+++ b/crates/ide/src/doc_links.rs
@@ -1,13 +1,27 @@
 //! Resolves and rewrites links in markdown documentation.
-//!
-//! Most of the implementation can be found in [`hir::doc_links`].
 
-use hir::{Adt, Crate, HasAttrs, ModuleDef};
-use ide_db::{defs::Definition, RootDatabase};
+use std::iter::once;
+
+use itertools::Itertools;
 use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
 use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
 use url::Url;
 
+use hir::{
+    db::{DefDatabase, HirDatabase},
+    Adt, AsAssocItem, AsName, AssocItem, AssocItemContainer, Crate, Field, HasAttrs, ItemInNs,
+    ModuleDef,
+};
+use ide_db::{
+    defs::{classify_name, classify_name_ref, Definition},
+    RootDatabase,
+};
+use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
+
+use crate::{FilePosition, Semantics};
+
+pub type DocumentationLink = String;
+
 /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
 pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
     let doc = Parser::new_with_broken_link_callback(
@@ -80,6 +94,70 @@ pub fn remove_links(markdown: &str) -> String {
     out
 }
 
+// FIXME:
+// BUG: For Option::Some
+// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
+// Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
+//
+// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
+// https://github.com/rust-lang/rfcs/pull/2988
+fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
+    // Get the outermost definition for the moduledef. This is used to resolve the public path to the type,
+    // then we can join the method, field, etc onto it if required.
+    let target_def: ModuleDef = match definition {
+        Definition::ModuleDef(moddef) => match moddef {
+            ModuleDef::Function(f) => f
+                .as_assoc_item(db)
+                .and_then(|assoc| match assoc.container(db) {
+                    AssocItemContainer::Trait(t) => Some(t.into()),
+                    AssocItemContainer::ImplDef(impld) => {
+                        impld.target_ty(db).as_adt().map(|adt| adt.into())
+                    }
+                })
+                .unwrap_or_else(|| f.clone().into()),
+            moddef => moddef,
+        },
+        Definition::Field(f) => f.parent_def(db).into(),
+        // FIXME: Handle macros
+        _ => return None,
+    };
+
+    let ns = ItemInNs::from(target_def.clone());
+
+    let module = definition.module(db)?;
+    let krate = module.krate();
+    let import_map = db.import_map(krate.into());
+    let base = once(krate.declaration_name(db)?.to_string())
+        .chain(import_map.path_of(ns)?.segments.iter().map(|name| name.to_string()))
+        .join("/");
+
+    let filename = get_symbol_filename(db, &target_def);
+    let fragment = match definition {
+        Definition::ModuleDef(moddef) => match moddef {
+            ModuleDef::Function(f) => {
+                get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Function(f)))
+            }
+            ModuleDef::Const(c) => {
+                get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Const(c)))
+            }
+            ModuleDef::TypeAlias(ty) => {
+                get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::TypeAlias(ty)))
+            }
+            _ => None,
+        },
+        Definition::Field(field) => get_symbol_fragment(db, &FieldOrAssocItem::Field(field)),
+        _ => None,
+    };
+
+    get_doc_url(db, &krate)
+        .and_then(|url| url.join(&base).ok())
+        .and_then(|url| filename.as_deref().and_then(|f| url.join(f).ok()))
+        .and_then(
+            |url| if let Some(fragment) = fragment { url.join(&fragment).ok() } else { Some(url) },
+        )
+        .map(|url| url.into_string())
+}
+
 fn rewrite_intra_doc_link(
     db: &RootDatabase,
     def: Definition,
@@ -138,7 +216,29 @@ fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<S
         .map(|url| url.into_string())
 }
 
-// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles.
+/// Retrieve a link to documentation for the given symbol.
+pub(crate) fn external_docs(
+    db: &RootDatabase,
+    position: &FilePosition,
+) -> Option<DocumentationLink> {
+    let sema = Semantics::new(db);
+    let file = sema.parse(position.file_id).syntax().clone();
+    let token = pick_best(file.token_at_offset(position.offset))?;
+    let token = sema.descend_into_macros(token);
+
+    let node = token.parent();
+    let definition = match_ast! {
+        match node {
+            ast::NameRef(name_ref) => classify_name_ref(&sema, &name_ref).map(|d| d.definition(sema.db)),
+            ast::Name(name) => classify_name(&sema, &name).map(|d| d.definition(sema.db)),
+            _ => None,
+        }
+    };
+
+    get_doc_link(db, definition?)
+}
+
+/// Rewrites a markdown document, applying 'callback' to each link.
 fn map_links<'e>(
     events: impl Iterator<Item = Event<'e>>,
     callback: impl Fn(&str, &str) -> (String, String),
@@ -239,6 +339,12 @@ fn ns_from_intra_spec(s: &str) -> Option<hir::Namespace> {
     .next()
 }
 
+/// Get the root URL for the documentation of a crate.
+///
+/// ```
+/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
+/// ^^^^^^^^^^^^^^^^^^^^^^^^^^
+/// ```
 fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
     krate
         .get_html_root_url(db)
@@ -255,8 +361,11 @@ fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
 
 /// Get the filename and extension generated for a symbol by rustdoc.
 ///
-/// Example: `struct.Shard.html`
-fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<String> {
+/// ```
+/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
+///                                    ^^^^^^^^^^^^^^^^^^^
+/// ```
+fn get_symbol_filename(db: &dyn HirDatabase, definition: &ModuleDef) -> Option<String> {
     Some(match definition {
         ModuleDef::Adt(adt) => match adt {
             Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
@@ -266,7 +375,7 @@ fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<Stri
         ModuleDef::Module(_) => "index.html".to_string(),
         ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
         ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
-        ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t),
+        ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.as_name()),
         ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
         ModuleDef::EnumVariant(ev) => {
             format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
@@ -275,3 +384,163 @@ fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<Stri
         ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
     })
 }
+
+enum FieldOrAssocItem {
+    Field(Field),
+    AssocItem(AssocItem),
+}
+
+/// Get the fragment required to link to a specific field, method, associated type, or associated constant.
+///
+/// ```
+/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
+///                                                       ^^^^^^^^^^^^^^
+/// ```
+fn get_symbol_fragment(db: &dyn HirDatabase, field_or_assoc: &FieldOrAssocItem) -> Option<String> {
+    Some(match field_or_assoc {
+        FieldOrAssocItem::Field(field) => format!("#structfield.{}", field.name(db)),
+        FieldOrAssocItem::AssocItem(assoc) => match assoc {
+            AssocItem::Function(function) => {
+                let is_trait_method = matches!(
+                    function.as_assoc_item(db).map(|assoc| assoc.container(db)),
+                    Some(AssocItemContainer::Trait(..))
+                );
+                // This distinction may get more complicated when specialisation is available.
+                // Rustdoc makes this decision based on whether a method 'has defaultness'.
+                // Currently this is only the case for provided trait methods.
+                if is_trait_method && !function.has_body(db) {
+                    format!("#tymethod.{}", function.name(db))
+                } else {
+                    format!("#method.{}", function.name(db))
+                }
+            }
+            AssocItem::Const(constant) => format!("#associatedconstant.{}", constant.name(db)?),
+            AssocItem::TypeAlias(ty) => format!("#associatedtype.{}", ty.name(db)),
+        },
+    })
+}
+
+fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
+    return tokens.max_by_key(priority);
+    fn priority(n: &SyntaxToken) -> usize {
+        match n.kind() {
+            IDENT | INT_NUMBER => 3,
+            T!['('] | T![')'] => 2,
+            kind if kind.is_trivia() => 0,
+            _ => 1,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use expect_test::{expect, Expect};
+
+    use crate::fixture;
+
+    fn check(ra_fixture: &str, expect: Expect) {
+        let (analysis, position) = fixture::position(ra_fixture);
+        let url = analysis.external_docs(position).unwrap().expect("could not find url for symbol");
+
+        expect.assert_eq(&url)
+    }
+
+    #[test]
+    fn test_doc_url_struct() {
+        check(
+            r#"
+pub struct Fo<|>o;
+"#,
+            expect![[r#"https://docs.rs/test/*/test/struct.Foo.html"#]],
+        );
+    }
+
+    #[test]
+    fn test_doc_url_fn() {
+        check(
+            r#"
+pub fn fo<|>o() {}
+"#,
+            expect![[r##"https://docs.rs/test/*/test/fn.foo.html#method.foo"##]],
+        );
+    }
+
+    #[test]
+    fn test_doc_url_inherent_method() {
+        check(
+            r#"
+pub struct Foo;
+
+impl Foo {
+    pub fn met<|>hod() {}
+}
+
+"#,
+            expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#method.method"##]],
+        );
+    }
+
+    #[test]
+    fn test_doc_url_trait_provided_method() {
+        check(
+            r#"
+pub trait Bar {
+    fn met<|>hod() {}
+}
+
+"#,
+            expect![[r##"https://docs.rs/test/*/test/trait.Bar.html#method.method"##]],
+        );
+    }
+
+    #[test]
+    fn test_doc_url_trait_required_method() {
+        check(
+            r#"
+pub trait Foo {
+    fn met<|>hod();
+}
+
+"#,
+            expect![[r##"https://docs.rs/test/*/test/trait.Foo.html#tymethod.method"##]],
+        );
+    }
+
+    #[test]
+    fn test_doc_url_field() {
+        check(
+            r#"
+pub struct Foo {
+    pub fie<|>ld: ()
+}
+
+"#,
+            expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#structfield.field"##]],
+        );
+    }
+
+    // FIXME: ImportMap will return re-export paths instead of public module
+    // paths. The correct path to documentation will never be a re-export.
+    // This problem stops us from resolving stdlib items included in the prelude
+    // such as `Option::Some` correctly.
+    #[ignore = "ImportMap may return re-exports"]
+    #[test]
+    fn test_reexport_order() {
+        check(
+            r#"
+pub mod wrapper {
+    pub use module::Item;
+
+    pub mod module {
+        pub struct Item;
+    }
+}
+
+fn foo() {
+    let bar: wrapper::It<|>em;
+}
+        "#,
+            expect![[r#"https://docs.rs/test/*/test/wrapper/module/struct.Item.html"#]],
+        )
+    }
+}
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 53265488e6..6290b35bd8 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::{remove_links, rewrite_links},
+    doc_links::{remove_links, rewrite_links},
     markdown_remove::remove_markdown,
     markup::Markup,
     runnables::runnable,
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 57f3581b6b..5db6e1311f 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -45,8 +45,8 @@ mod status;
 mod syntax_highlighting;
 mod syntax_tree;
 mod typing;
-mod link_rewrite;
 mod markdown_remove;
+mod doc_links;
 
 use std::sync::Arc;
 
@@ -382,6 +382,14 @@ impl Analysis {
         self.with_db(|db| hover::hover(db, position, links_in_hover, markdown))
     }
 
+    /// Return URL(s) for the documentation of the symbol under the cursor.
+    pub fn external_docs(
+        &self,
+        position: FilePosition,
+    ) -> Cancelable<Option<doc_links::DocumentationLink>> {
+        self.with_db(|db| doc_links::external_docs(db, &position))
+    }
+
     /// Computes parameter information for the given call expression.
     pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> {
         self.with_db(|db| call_info::call_info(db, position))
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index 468655f9c3..ba73abcacd 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -1310,6 +1310,18 @@ pub(crate) fn handle_semantic_tokens_range(
     Ok(Some(semantic_tokens.into()))
 }
 
+pub(crate) fn handle_open_docs(
+    snap: GlobalStateSnapshot,
+    params: lsp_types::TextDocumentPositionParams,
+) -> Result<Option<lsp_types::Url>> {
+    let _p = profile::span("handle_open_docs");
+    let position = from_proto::file_position(&snap, params)?;
+
+    let remote = snap.analysis.external_docs(position)?;
+
+    Ok(remote.and_then(|remote| Url::parse(&remote).ok()))
+}
+
 fn implementation_title(count: usize) -> String {
     if count == 1 {
         "1 implementation".into()
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index fee0bb69c7..f31f8d9001 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -347,3 +347,11 @@ pub struct CommandLink {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub tooltip: Option<String>,
 }
+
+pub enum ExternalDocs {}
+
+impl Request for ExternalDocs {
+    type Params = lsp_types::TextDocumentPositionParams;
+    type Result = Option<lsp_types::Url>;
+    const METHOD: &'static str = "experimental/externalDocs";
+}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 4b7ac8224e..06b38d99c8 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -384,6 +384,7 @@ impl GlobalState {
             .on::<lsp_ext::CodeActionRequest>(handlers::handle_code_action)?
             .on::<lsp_ext::ResolveCodeActionRequest>(handlers::handle_resolve_code_action)?
             .on::<lsp_ext::HoverRequest>(handlers::handle_hover)?
+            .on::<lsp_ext::ExternalDocs>(handlers::handle_open_docs)?
             .on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)?
             .on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)?
             .on::<lsp_types::request::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
diff --git a/crates/stdx/src/macros.rs b/crates/stdx/src/macros.rs
index bf298460f3..f5ee3484b0 100644
--- a/crates/stdx/src/macros.rs
+++ b/crates/stdx/src/macros.rs
@@ -18,7 +18,13 @@ macro_rules! format_to {
     };
 }
 
-// Generates `From` impls for `Enum E { Foo(Foo), Bar(Bar) }` enums
+/// Generates `From` impls for `Enum E { Foo(Foo), Bar(Bar) }` enums
+///
+/// # Example
+///
+/// ```rust
+/// impl_from!(Struct, Union, Enum for Adt);
+/// ```
 #[macro_export]
 macro_rules! impl_from {
     ($($variant:ident $(($($sub_variant:ident),*))?),* for $enum:ident) => {
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index f1160bb1cc..3f861f3e00 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -129,7 +129,7 @@ As a result of the command call the client will get the respective workspace edi
 
 **Server Capability:** `{ "parentModule": boolean }`
 
-This request is send from client to server to handle "Goto Parent Module" editor action.
+This request is sent from client to server to handle "Goto Parent Module" editor action.
 
 **Method:** `experimental/parentModule`
 
@@ -163,7 +163,7 @@ mod foo;
 
 **Server Capability:** `{ "joinLines": boolean }`
 
-This request is send from client to server to handle "Join Lines" editor action.
+This request is sent from client to server to handle "Join Lines" editor action.
 
 **Method:** `experimental/joinLines`
 
@@ -210,7 +210,7 @@ fn main() {
 
 **Server Capability:** `{ "onEnter": boolean }`
 
-This request is send from client to server to handle <kbd>Enter</kbd> keypress.
+This request is sent from client to server to handle <kbd>Enter</kbd> keypress.
 
 **Method:** `experimental/onEnter`
 
@@ -261,7 +261,7 @@ As proper cursor positioning is raison-d'etat for `onEnter`, it uses `SnippetTex
 
 **Server Capability:** `{ "ssr": boolean }`
 
-This request is send from client to server to handle structural search replace -- automated syntax tree based transformation of the source.
+This request is sent from client to server to handle structural search replace -- automated syntax tree based transformation of the source.
 
 **Method:** `experimental/ssr`
 
@@ -303,7 +303,7 @@ SSR with query `foo($a, $b) ==>> ($a).foo($b)` will transform, eg `foo(y + 5, z)
 
 **Server Capability:** `{ "matchingBrace": boolean }`
 
-This request is send from client to server to handle "Matching Brace" editor action.
+This request is sent from client to server to handle "Matching Brace" editor action.
 
 **Method:** `experimental/matchingBrace`
 
@@ -348,7 +348,7 @@ Moreover, it would be cool if editors didn't need to implement even basic langua
 
 **Server Capability:** `{ "runnables": { "kinds": string[] } }`
 
-This request is send from client to server to get the list of things that can be run (tests, binaries, `cargo check -p`).
+This request is sent from client to server to get the list of things that can be run (tests, binaries, `cargo check -p`).
 
 **Method:** `experimental/runnables`
 
@@ -386,6 +386,17 @@ rust-analyzer supports only one `kind`, `"cargo"`. The `args` for `"cargo"` look
 }
 ```
 
+## Open External Documentation
+
+This request is sent from client to server to get a URL to documentation for the symbol under the cursor, if available.
+
+**Method** `experimental/externalDocs`
+
+**Request:**: `TextDocumentPositionParams`
+
+**Response** `string | null`
+
+
 ## Analyzer Status
 
 **Method:** `rust-analyzer/analyzerStatus`
@@ -477,7 +488,7 @@ Expands macro call at a given position.
 
 **Method:** `rust-analyzer/inlayHints`
 
-This request is send from client to server to render "inlay hints" -- virtual text inserted into editor to show things like inferred types.
+This request is sent from client to server to render "inlay hints" -- virtual text inserted into editor to show things like inferred types.
 Generally, the client should re-query inlay hints after every modification.
 Note that we plan to move this request to `experimental/inlayHints`, as it is not really Rust-specific, but the current API is not necessary the right one.
 Upstream issue: https://github.com/microsoft/language-server-protocol/issues/956
diff --git a/editors/code/package.json b/editors/code/package.json
index 6a712a8a82..4bd3117fc8 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -182,6 +182,11 @@
                 "command": "rust-analyzer.toggleInlayHints",
                 "title": "Toggle inlay hints",
                 "category": "Rust Analyzer"
+            },
+            {
+                "command": "rust-analyzer.openDocs",
+                "title": "Open docs under cursor",
+                "category": "Rust Analyzer"
             }
         ],
         "keybindings": [
@@ -1044,6 +1049,10 @@
                 {
                     "command": "rust-analyzer.toggleInlayHints",
                     "when": "inRustProject"
+                },
+                {
+                    "command": "rust-analyzer.openDocs",
+                    "when": "inRustProject"
                 }
             ]
         }
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 1a90f1b7d9..1445e41d3c 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -419,10 +419,31 @@ export function gotoLocation(ctx: Ctx): Cmd {
     };
 }
 
+export function openDocs(ctx: Ctx): Cmd {
+    return async () => {
+
+        const client = ctx.client;
+        const editor = vscode.window.activeTextEditor;
+        if (!editor || !client) {
+            return;
+        };
+
+        const position = editor.selection.active;
+        const textDocument = { uri: editor.document.uri.toString() };
+
+        const doclink = await client.sendRequest(ra.openDocs, { position, textDocument });
+
+        if (doclink != null) {
+            vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(doclink));
+        }
+    };
+
+}
+
 export function resolveCodeAction(ctx: Ctx): Cmd {
     const client = ctx.client;
-    return async (params: ra.ResolveCodeActionParams) => {
-        const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, params);
+    return async () => {
+        const item: lc.WorkspaceEdit = await client.sendRequest(ra.resolveCodeAction, null);
         if (!item) {
             return;
         }
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index f286b68a68..fc8e120b3f 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -118,3 +118,5 @@ export interface CommandLinkGroup {
     title?: string;
     commands: CommandLink[];
 }
+
+export const openDocs = new lc.RequestType<lc.TextDocumentPositionParams, string | void, void>('experimental/externalDocs');
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 2896d90ac9..09543e348a 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -110,6 +110,7 @@ async function tryActivate(context: vscode.ExtensionContext) {
     ctx.registerCommand('run', commands.run);
     ctx.registerCommand('debug', commands.debug);
     ctx.registerCommand('newDebugConfig', commands.newDebugConfig);
+    ctx.registerCommand('openDocs', commands.openDocs);
 
     defaultOnEnter.dispose();
     ctx.registerCommand('onEnter', commands.onEnter);


Test patch:
diff --git a/crates/hir_def/src/item_tree/tests.rs b/crates/hir_def/src/item_tree/tests.rs
index 1a806cda52..4b354c4c14 100644
--- a/crates/hir_def/src/item_tree/tests.rs
+++ b/crates/hir_def/src/item_tree/tests.rs
@@ -240,9 +240,9 @@ fn smoke() {
             > #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("assoc_const"))] }, input: None }]) }]
             > Const { name: Some(Name(Text("CONST"))), visibility: RawVisibilityId("pub(self)"), type_ref: Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("u8"))] }, generic_args: [None] }), ast_id: FileAstId::<syntax::ast::generated::nodes::Const>(9) }
             > #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("assoc_method"))] }, input: None }]) }]
-            > Function { name: Name(Text("method")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: true, is_unsafe: false, params: [Reference(Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("Self"))] }, generic_args: [None] }), Shared)], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(10) }
+            > Function { name: Name(Text("method")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: true, has_body: false, is_unsafe: false, params: [Reference(Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("Self"))] }, generic_args: [None] }), Shared)], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(10) }
             > #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("assoc_dfl_method"))] }, input: None }]) }]
-            > Function { name: Name(Text("dfl_method")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: true, is_unsafe: false, params: [Reference(Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("Self"))] }, generic_args: [None] }), Mut)], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(11) }
+            > Function { name: Name(Text("dfl_method")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: true, has_body: true, is_unsafe: false, params: [Reference(Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("Self"))] }, generic_args: [None] }), Mut)], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(11) }
             #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("struct0"))] }, input: None }]) }]
             Struct { name: Name(Text("Struct0")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(1), fields: Unit, ast_id: FileAstId::<syntax::ast::generated::nodes::Struct>(3), kind: Unit }
             #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("struct1"))] }, input: None }]) }]
@@ -275,12 +275,12 @@ fn simple_inner_items() {
 
             top-level items:
             Impl { generic_params: GenericParamsId(0), target_trait: Some(Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("D"))] }, generic_args: [None] })), target_type: Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("Response"))] }, generic_args: [Some(GenericArgs { args: [Type(Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("T"))] }, generic_args: [None] }))], has_self_type: false, bindings: [] })] }), is_negative: false, items: [Function(Idx::<Function>(1))], ast_id: FileAstId::<syntax::ast::generated::nodes::Impl>(0) }
-            > Function { name: Name(Text("foo")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
+            > Function { name: Name(Text("foo")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
 
             inner items:
 
             for AST FileAstId::<syntax::ast::generated::nodes::Item>(2):
-            Function { name: Name(Text("end")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(1), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
+            Function { name: Name(Text("end")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(1), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
 
         "#]],
     );
@@ -303,9 +303,9 @@ fn extern_attrs() {
 
             top-level items:
             #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("attr_a"))] }, input: None }, Attr { path: ModPath { kind: Plain, segments: [Name(Text("block_attr"))] }, input: None }]) }]
-            Function { name: Name(Text("a")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: true, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
+            Function { name: Name(Text("a")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: true, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
             #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("attr_b"))] }, input: None }, Attr { path: ModPath { kind: Plain, segments: [Name(Text("block_attr"))] }, input: None }]) }]
-            Function { name: Name(Text("b")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: true, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
+            Function { name: Name(Text("b")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: true, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
         "##]],
     );
 }
@@ -329,9 +329,9 @@ fn trait_attrs() {
             #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("trait_attr"))] }, input: None }]) }]
             Trait { name: Name(Text("Tr")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(0), auto: false, items: [Function(Idx::<Function>(0)), Function(Idx::<Function>(1))], ast_id: FileAstId::<syntax::ast::generated::nodes::Trait>(0) }
             > #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("attr_a"))] }, input: None }]) }]
-            > Function { name: Name(Text("a")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
+            > Function { name: Name(Text("a")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
             > #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("attr_b"))] }, input: None }]) }]
-            > Function { name: Name(Text("b")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
+            > Function { name: Name(Text("b")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
         "##]],
     );
 }
@@ -355,9 +355,9 @@ fn impl_attrs() {
             #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("impl_attr"))] }, input: None }]) }]
             Impl { generic_params: GenericParamsId(4294967295), target_trait: None, target_type: Path(Path { type_anchor: None, mod_path: ModPath { kind: Plain, segments: [Name(Text("Ty"))] }, generic_args: [None] }), is_negative: false, items: [Function(Idx::<Function>(0)), Function(Idx::<Function>(1))], ast_id: FileAstId::<syntax::ast::generated::nodes::Impl>(0) }
             > #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("attr_a"))] }, input: None }]) }]
-            > Function { name: Name(Text("a")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
+            > Function { name: Name(Text("a")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
             > #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("attr_b"))] }, input: None }]) }]
-            > Function { name: Name(Text("b")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
+            > Function { name: Name(Text("b")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(2) }
         "##]],
     );
 }
@@ -408,13 +408,13 @@ fn inner_item_attrs() {
             inner attrs: Attrs { entries: None }
 
             top-level items:
-            Function { name: Name(Text("foo")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(0) }
+            Function { name: Name(Text("foo")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(0) }
 
             inner items:
 
             for AST FileAstId::<syntax::ast::generated::nodes::Item>(1):
             #[Attrs { entries: Some([Attr { path: ModPath { kind: Plain, segments: [Name(Text("on_inner"))] }, input: None }]) }]
-            Function { name: Name(Text("inner")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
+            Function { name: Name(Text("inner")), visibility: RawVisibilityId("pub(self)"), generic_params: GenericParamsId(4294967295), has_self_param: false, has_body: true, is_unsafe: false, params: [], is_varargs: false, ret_type: Tuple([]), ast_id: FileAstId::<syntax::ast::generated::nodes::Fn>(1) }
 
         "##]],
     );