5917: Add a command to open docs for the symbol under the cursor r=matklad a=zacps

#### 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


Co-authored-by: Zac Pullar-Strecker <zacmps@gmail.com>
This commit is contained in:
bors[bot] 2020-10-12 07:38:24 +00:00 committed by GitHub
commit 518f6d7724
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 403 additions and 32 deletions

View File

@ -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.

View File

@ -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(),

View File

@ -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,

View File

@ -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,

View File

@ -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) }
"##]],
);

View File

@ -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"#]],
)
}
}

View File

@ -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,

View File

@ -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;
@ -384,6 +384,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))

View File

@ -1301,6 +1301,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()

View File

@ -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";
}

View File

@ -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)?

View File

@ -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) => {

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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;
}

View File

@ -118,3 +118,5 @@ export interface CommandLinkGroup {
title?: string;
commands: CommandLink[];
}
export const openDocs = new lc.RequestType<lc.TextDocumentPositionParams, string | void, void>('experimental/externalDocs');

View File

@ -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);