mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-02 06:41:48 +00:00
Merge #4660
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  Co-authored-by: Aaron Loucks <aloucks@cofront.net>
This commit is contained in:
commit
c6b739bad0
7 changed files with 260 additions and 19 deletions
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue