mirror of
https://github.com/rust-lang/rust.git
synced 2025-02-22 11:53:44 +00:00
Merge #5917
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:
commit
518f6d7724
@ -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.
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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) }
|
||||
|
||||
"##]],
|
||||
);
|
||||
|
@ -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"#]],
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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)?
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -118,3 +118,5 @@ export interface CommandLinkGroup {
|
||||
title?: string;
|
||||
commands: CommandLink[];
|
||||
}
|
||||
|
||||
export const openDocs = new lc.RequestType<lc.TextDocumentPositionParams, string | void, void>('experimental/externalDocs');
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user