4660: Enable hover and autocomplete docs on macro generated items r=aloucks a=aloucks

Enable hover and autocomplete docs on macro generated items. This de-sugars doc comments into `doc` attributes in some cases, but not all. Comments and `doc` attributes are then merged together. 

This PR is essentially a partial implementation of what's being suggested #3182, but it's not all the way there yet. ~I still need to add unit tests~, but I wanted to first get feedback on whether or not this was an acceptable path forward.

Fixes #4564
Fixes #3984
Fixes #3180
Related #3182

![macro_item_docs](https://user-images.githubusercontent.com/221559/83336760-15012200-a284-11ea-8d0d-b6a615850044.gif)



Co-authored-by: Aaron Loucks <aloucks@cofront.net>
This commit is contained in:
bors[bot] 2020-06-03 11:05:52 +00:00 committed by GitHub
commit c6b739bad0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 260 additions and 19 deletions

View file

@ -87,12 +87,18 @@ impl Attrs {
} }
pub(crate) fn new(owner: &dyn AttrsOwner, hygiene: &Hygiene) -> Attrs { pub(crate) fn new(owner: &dyn AttrsOwner, hygiene: &Hygiene) -> Attrs {
let docs = ast::CommentIter::from_syntax_node(owner.syntax()).doc_comment_text().map(
|docs_text| Attr {
input: Some(AttrInput::Literal(SmolStr::new(docs_text))),
path: ModPath::from(hir_expand::name!(doc)),
},
);
let mut attrs = owner.attrs().peekable(); let mut attrs = owner.attrs().peekable();
let entries = if attrs.peek().is_none() { let entries = if attrs.peek().is_none() {
// Avoid heap allocation // Avoid heap allocation
None None
} else { } else {
Some(attrs.flat_map(|ast| Attr::from_src(ast, hygiene)).collect()) Some(attrs.flat_map(|ast| Attr::from_src(ast, hygiene)).chain(docs).collect())
}; };
Attrs { entries } Attrs { entries }
} }

View file

@ -29,6 +29,13 @@ impl Documentation {
Documentation(s.into()) Documentation(s.into())
} }
pub fn from_ast<N>(node: &N) -> Option<Documentation>
where
N: ast::DocCommentsOwner + ast::AttrsOwner,
{
docs_from_ast(node)
}
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&*self.0 &*self.0
} }
@ -70,6 +77,45 @@ impl Documentation {
} }
} }
pub(crate) fn docs_from_ast(node: &impl ast::DocCommentsOwner) -> Option<Documentation> { pub(crate) fn docs_from_ast<N>(node: &N) -> Option<Documentation>
node.doc_comment_text().map(|it| Documentation::new(&it)) where
N: ast::DocCommentsOwner + ast::AttrsOwner,
{
let doc_comment_text = node.doc_comment_text();
let doc_attr_text = expand_doc_attrs(node);
let docs = merge_doc_comments_and_attrs(doc_comment_text, doc_attr_text);
docs.map(|it| Documentation::new(&it))
}
fn merge_doc_comments_and_attrs(
doc_comment_text: Option<String>,
doc_attr_text: Option<String>,
) -> Option<String> {
match (doc_comment_text, doc_attr_text) {
(Some(mut comment_text), Some(attr_text)) => {
comment_text.push_str("\n\n");
comment_text.push_str(&attr_text);
Some(comment_text)
}
(Some(comment_text), None) => Some(comment_text),
(None, Some(attr_text)) => Some(attr_text),
(None, None) => None,
}
}
fn expand_doc_attrs(owner: &dyn ast::AttrsOwner) -> Option<String> {
let mut docs = String::new();
for attr in owner.attrs() {
if let Some(("doc", value)) =
attr.as_simple_key_value().as_ref().map(|(k, v)| (k.as_str(), v.as_str()))
{
docs.push_str(value);
docs.push_str("\n\n");
}
}
if docs.is_empty() {
None
} else {
Some(docs.trim_end_matches("\n\n").to_owned())
}
} }

View file

@ -153,6 +153,7 @@ pub mod known {
str, str,
// Special names // Special names
macro_rules, macro_rules,
doc,
// Components of known path (value or mod name) // Components of known path (value or mod name)
std, std,
core, core,

View file

@ -125,3 +125,81 @@ pub(crate) fn completions(
Some(acc) Some(acc)
} }
#[cfg(test)]
mod tests {
use crate::completion::completion_config::CompletionConfig;
use crate::mock_analysis::analysis_and_position;
struct DetailAndDocumentation<'a> {
detail: &'a str,
documentation: &'a str,
}
fn check_detail_and_documentation(fixture: &str, expected: DetailAndDocumentation) {
let (analysis, position) = analysis_and_position(fixture);
let config = CompletionConfig::default();
let completions = analysis.completions(&config, position).unwrap().unwrap();
for item in completions {
if item.detail() == Some(expected.detail) {
let opt = item.documentation();
let doc = opt.as_ref().map(|it| it.as_str());
assert_eq!(doc, Some(expected.documentation));
return;
}
}
panic!("completion detail not found: {}", expected.detail)
}
#[test]
fn test_completion_detail_from_macro_generated_struct_fn_doc_attr() {
check_detail_and_documentation(
r#"
//- /lib.rs
macro_rules! bar {
() => {
struct Bar;
impl Bar {
#[doc = "Do the foo"]
fn foo(&self) {}
}
}
}
bar!();
fn foo() {
let bar = Bar;
bar.fo<|>;
}
"#,
DetailAndDocumentation { detail: "fn foo(&self)", documentation: "Do the foo" },
);
}
#[test]
fn test_completion_detail_from_macro_generated_struct_fn_doc_comment() {
check_detail_and_documentation(
r#"
//- /lib.rs
macro_rules! bar {
() => {
struct Bar;
impl Bar {
/// Do the foo
fn foo(&self) {}
}
}
}
bar!();
fn foo() {
let bar = Bar;
bar.fo<|>;
}
"#,
DetailAndDocumentation { detail: "fn foo(&self)", documentation: " Do the foo" },
);
}
}

View file

@ -1,8 +1,8 @@
use std::iter::once; use std::iter::once;
use hir::{ use hir::{
Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef, Adt, AsAssocItem, AssocItemContainer, Documentation, FieldSource, HasSource, HirDisplay,
ModuleSource, Semantics, ModuleDef, ModuleSource, Semantics,
}; };
use itertools::Itertools; use itertools::Itertools;
use ra_db::SourceDatabase; use ra_db::SourceDatabase;
@ -10,12 +10,7 @@ use ra_ide_db::{
defs::{classify_name, classify_name_ref, Definition}, defs::{classify_name, classify_name_ref, Definition},
RootDatabase, RootDatabase,
}; };
use ra_syntax::{ use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset};
ast::{self, DocCommentsOwner},
match_ast, AstNode,
SyntaxKind::*,
SyntaxToken, TokenAtOffset,
};
use crate::{ use crate::{
display::{macro_label, rust_code_markup, rust_code_markup_with_doc, ShortLabel}, display::{macro_label, rust_code_markup, rust_code_markup_with_doc, ShortLabel},
@ -169,13 +164,15 @@ fn hover_text_from_name_kind(db: &RootDatabase, def: Definition) -> Option<Strin
return match def { return match def {
Definition::Macro(it) => { Definition::Macro(it) => {
let src = it.source(db); let src = it.source(db);
hover_text(src.value.doc_comment_text(), Some(macro_label(&src.value)), mod_path) let docs = Documentation::from_ast(&src.value).map(Into::into);
hover_text(docs, Some(macro_label(&src.value)), mod_path)
} }
Definition::Field(it) => { Definition::Field(it) => {
let src = it.source(db); let src = it.source(db);
match src.value { match src.value {
FieldSource::Named(it) => { FieldSource::Named(it) => {
hover_text(it.doc_comment_text(), it.short_label(), mod_path) let docs = Documentation::from_ast(&it).map(Into::into);
hover_text(docs, it.short_label(), mod_path)
} }
_ => None, _ => None,
} }
@ -183,7 +180,8 @@ fn hover_text_from_name_kind(db: &RootDatabase, def: Definition) -> Option<Strin
Definition::ModuleDef(it) => match it { Definition::ModuleDef(it) => match it {
ModuleDef::Module(it) => match it.definition_source(db).value { ModuleDef::Module(it) => match it.definition_source(db).value {
ModuleSource::Module(it) => { ModuleSource::Module(it) => {
hover_text(it.doc_comment_text(), it.short_label(), mod_path) let docs = Documentation::from_ast(&it).map(Into::into);
hover_text(docs, it.short_label(), mod_path)
} }
_ => None, _ => None,
}, },
@ -208,10 +206,11 @@ fn hover_text_from_name_kind(db: &RootDatabase, def: Definition) -> Option<Strin
fn from_def_source<A, D>(db: &RootDatabase, def: D, mod_path: Option<String>) -> Option<String> fn from_def_source<A, D>(db: &RootDatabase, def: D, mod_path: Option<String>) -> Option<String>
where where
D: HasSource<Ast = A>, D: HasSource<Ast = A>,
A: ast::DocCommentsOwner + ast::NameOwner + ShortLabel, A: ast::DocCommentsOwner + ast::NameOwner + ShortLabel + ast::AttrsOwner,
{ {
let src = def.source(db); let src = def.source(db);
hover_text(src.value.doc_comment_text(), src.value.short_label(), mod_path) let docs = Documentation::from_ast(&src.value).map(Into::into);
hover_text(docs, src.value.short_label(), mod_path)
} }
} }
@ -951,4 +950,106 @@ fn func(foo: i32) { if true { <|>foo; }; }
&["mod my"], &["mod my"],
); );
} }
#[test]
fn test_hover_struct_doc_comment() {
check_hover_result(
r#"
//- /lib.rs
/// bar docs
struct Bar;
fn foo() {
let bar = Ba<|>r;
}
"#,
&["struct Bar\n```\n___\n\nbar docs"],
);
}
#[test]
fn test_hover_struct_doc_attr() {
check_hover_result(
r#"
//- /lib.rs
#[doc = "bar docs"]
struct Bar;
fn foo() {
let bar = Ba<|>r;
}
"#,
&["struct Bar\n```\n___\n\nbar docs"],
);
}
#[test]
fn test_hover_struct_doc_attr_multiple_and_mixed() {
check_hover_result(
r#"
//- /lib.rs
/// bar docs 0
#[doc = "bar docs 1"]
#[doc = "bar docs 2"]
struct Bar;
fn foo() {
let bar = Ba<|>r;
}
"#,
&["struct Bar\n```\n___\n\nbar docs 0\n\nbar docs 1\n\nbar docs 2"],
);
}
#[test]
fn test_hover_macro_generated_struct_fn_doc_comment() {
check_hover_result(
r#"
//- /lib.rs
macro_rules! bar {
() => {
struct Bar;
impl Bar {
/// Do the foo
fn foo(&self) {}
}
}
}
bar!();
fn foo() {
let bar = Bar;
bar.fo<|>o();
}
"#,
&["Bar\n```\n\n```rust\nfn foo(&self)\n```\n___\n\n Do the foo"],
);
}
#[test]
fn test_hover_macro_generated_struct_fn_doc_attr() {
check_hover_result(
r#"
//- /lib.rs
macro_rules! bar {
() => {
struct Bar;
impl Bar {
#[doc = "Do the foo"]
fn foo(&self) {}
}
}
}
bar!();
fn foo() {
let bar = Bar;
bar.fo<|>o();
}
"#,
&["Bar\n```\n\n```rust\nfn foo(&self)\n```\n___\n\nDo the foo"],
);
}
} }

View file

@ -83,13 +83,22 @@ pub trait DocCommentsOwner: AstNode {
CommentIter { iter: self.syntax().children_with_tokens() } CommentIter { iter: self.syntax().children_with_tokens() }
} }
fn doc_comment_text(&self) -> Option<String> {
self.doc_comments().doc_comment_text()
}
}
impl CommentIter {
pub fn from_syntax_node(syntax_node: &ast::SyntaxNode) -> CommentIter {
CommentIter { iter: syntax_node.children_with_tokens() }
}
/// Returns the textual content of a doc comment block as a single string. /// Returns the textual content of a doc comment block as a single string.
/// That is, strips leading `///` (+ optional 1 character of whitespace), /// That is, strips leading `///` (+ optional 1 character of whitespace),
/// trailing `*/`, trailing whitespace and then joins the lines. /// trailing `*/`, trailing whitespace and then joins the lines.
fn doc_comment_text(&self) -> Option<String> { pub fn doc_comment_text(self) -> Option<String> {
let mut has_comments = false; let mut has_comments = false;
let docs = self let docs = self
.doc_comments()
.filter(|comment| comment.kind().doc.is_some()) .filter(|comment| comment.kind().doc.is_some())
.map(|comment| { .map(|comment| {
has_comments = true; has_comments = true;

View file

@ -76,7 +76,7 @@ Navigates to the type of an identifier.
=== Hover === Hover
**Source:** https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/ra_ide/src/hover.rs#L63[hover.rs] **Source:** https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/ra_ide/src/hover.rs#L58[hover.rs]
Shows additional information, like type of an expression or documentation for definition when "focusing" code. Shows additional information, like type of an expression or documentation for definition when "focusing" code.
Focusing is usually hovering with a mouse, but can also be triggered with a shortcut. Focusing is usually hovering with a mouse, but can also be triggered with a shortcut.