Skip to content

Commit bc54d8d

Browse files
bors[bot]aloucks
andauthored
Merge #4660
4660: Enable hover and autocomplete docs on macro generated items r=matklad 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 <[email protected]>
2 parents 9b3d4be + 25cbc80 commit bc54d8d

File tree

6 files changed

+259
-18
lines changed

6 files changed

+259
-18
lines changed

crates/ra_hir_def/src/attr.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,18 @@ impl Attrs {
8787
}
8888

8989
pub(crate) fn new(owner: &dyn AttrsOwner, hygiene: &Hygiene) -> Attrs {
90+
let docs = ast::CommentIter::from_syntax_node(owner.syntax()).doc_comment_text().map(
91+
|docs_text| Attr {
92+
input: Some(AttrInput::Literal(SmolStr::new(docs_text))),
93+
path: ModPath::from(hir_expand::name!(doc)),
94+
},
95+
);
9096
let mut attrs = owner.attrs().peekable();
9197
let entries = if attrs.peek().is_none() {
9298
// Avoid heap allocation
9399
None
94100
} else {
95-
Some(attrs.flat_map(|ast| Attr::from_src(ast, hygiene)).collect())
101+
Some(attrs.flat_map(|ast| Attr::from_src(ast, hygiene)).chain(docs).collect())
96102
};
97103
Attrs { entries }
98104
}

crates/ra_hir_def/src/docs.rs

+48-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ impl Documentation {
2929
Documentation(s.into())
3030
}
3131

32+
pub fn from_ast<N>(node: &N) -> Option<Documentation>
33+
where
34+
N: ast::DocCommentsOwner + ast::AttrsOwner,
35+
{
36+
docs_from_ast(node)
37+
}
38+
3239
pub fn as_str(&self) -> &str {
3340
&*self.0
3441
}
@@ -70,6 +77,45 @@ impl Documentation {
7077
}
7178
}
7279

73-
pub(crate) fn docs_from_ast(node: &impl ast::DocCommentsOwner) -> Option<Documentation> {
74-
node.doc_comment_text().map(|it| Documentation::new(&it))
80+
pub(crate) fn docs_from_ast<N>(node: &N) -> Option<Documentation>
81+
where
82+
N: ast::DocCommentsOwner + ast::AttrsOwner,
83+
{
84+
let doc_comment_text = node.doc_comment_text();
85+
let doc_attr_text = expand_doc_attrs(node);
86+
let docs = merge_doc_comments_and_attrs(doc_comment_text, doc_attr_text);
87+
docs.map(|it| Documentation::new(&it))
88+
}
89+
90+
fn merge_doc_comments_and_attrs(
91+
doc_comment_text: Option<String>,
92+
doc_attr_text: Option<String>,
93+
) -> Option<String> {
94+
match (doc_comment_text, doc_attr_text) {
95+
(Some(mut comment_text), Some(attr_text)) => {
96+
comment_text.push_str("\n\n");
97+
comment_text.push_str(&attr_text);
98+
Some(comment_text)
99+
}
100+
(Some(comment_text), None) => Some(comment_text),
101+
(None, Some(attr_text)) => Some(attr_text),
102+
(None, None) => None,
103+
}
104+
}
105+
106+
fn expand_doc_attrs(owner: &dyn ast::AttrsOwner) -> Option<String> {
107+
let mut docs = String::new();
108+
for attr in owner.attrs() {
109+
if let Some(("doc", value)) =
110+
attr.as_simple_key_value().as_ref().map(|(k, v)| (k.as_str(), v.as_str()))
111+
{
112+
docs.push_str(value);
113+
docs.push_str("\n\n");
114+
}
115+
}
116+
if docs.is_empty() {
117+
None
118+
} else {
119+
Some(docs.trim_end_matches("\n\n").to_owned())
120+
}
75121
}

crates/ra_hir_expand/src/name.rs

+1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ pub mod known {
153153
str,
154154
// Special names
155155
macro_rules,
156+
doc,
156157
// Components of known path (value or mod name)
157158
std,
158159
core,

crates/ra_ide/src/completion.rs

+78
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,81 @@ pub(crate) fn completions(
125125

126126
Some(acc)
127127
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use crate::completion::completion_config::CompletionConfig;
132+
use crate::mock_analysis::analysis_and_position;
133+
134+
struct DetailAndDocumentation<'a> {
135+
detail: &'a str,
136+
documentation: &'a str,
137+
}
138+
139+
fn check_detail_and_documentation(fixture: &str, expected: DetailAndDocumentation) {
140+
let (analysis, position) = analysis_and_position(fixture);
141+
let config = CompletionConfig::default();
142+
let completions = analysis.completions(&config, position).unwrap().unwrap();
143+
for item in completions {
144+
if item.detail() == Some(expected.detail) {
145+
let opt = item.documentation();
146+
let doc = opt.as_ref().map(|it| it.as_str());
147+
assert_eq!(doc, Some(expected.documentation));
148+
return;
149+
}
150+
}
151+
panic!("completion detail not found: {}", expected.detail)
152+
}
153+
154+
#[test]
155+
fn test_completion_detail_from_macro_generated_struct_fn_doc_attr() {
156+
check_detail_and_documentation(
157+
r#"
158+
//- /lib.rs
159+
macro_rules! bar {
160+
() => {
161+
struct Bar;
162+
impl Bar {
163+
#[doc = "Do the foo"]
164+
fn foo(&self) {}
165+
}
166+
}
167+
}
168+
169+
bar!();
170+
171+
fn foo() {
172+
let bar = Bar;
173+
bar.fo<|>;
174+
}
175+
"#,
176+
DetailAndDocumentation { detail: "fn foo(&self)", documentation: "Do the foo" },
177+
);
178+
}
179+
180+
#[test]
181+
fn test_completion_detail_from_macro_generated_struct_fn_doc_comment() {
182+
check_detail_and_documentation(
183+
r#"
184+
//- /lib.rs
185+
macro_rules! bar {
186+
() => {
187+
struct Bar;
188+
impl Bar {
189+
/// Do the foo
190+
fn foo(&self) {}
191+
}
192+
}
193+
}
194+
195+
bar!();
196+
197+
fn foo() {
198+
let bar = Bar;
199+
bar.fo<|>;
200+
}
201+
"#,
202+
DetailAndDocumentation { detail: "fn foo(&self)", documentation: " Do the foo" },
203+
);
204+
}
205+
}

crates/ra_ide/src/hover.rs

+114-13
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
use std::iter::once;
22

33
use hir::{
4-
Adt, AsAssocItem, AssocItemContainer, FieldSource, HasSource, HirDisplay, ModuleDef,
5-
ModuleSource, Semantics,
4+
Adt, AsAssocItem, AssocItemContainer, Documentation, FieldSource, HasSource, HirDisplay,
5+
ModuleDef, ModuleSource, Semantics,
66
};
77
use itertools::Itertools;
88
use ra_db::SourceDatabase;
99
use ra_ide_db::{
1010
defs::{classify_name, classify_name_ref, Definition},
1111
RootDatabase,
1212
};
13-
use ra_syntax::{
14-
ast::{self, DocCommentsOwner},
15-
match_ast, AstNode,
16-
SyntaxKind::*,
17-
SyntaxToken, TokenAtOffset,
18-
};
13+
use ra_syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset};
1914

2015
use crate::{
2116
display::{macro_label, rust_code_markup, rust_code_markup_with_doc, ShortLabel},
@@ -169,21 +164,24 @@ fn hover_text_from_name_kind(db: &RootDatabase, def: Definition) -> Option<Strin
169164
return match def {
170165
Definition::Macro(it) => {
171166
let src = it.source(db);
172-
hover_text(src.value.doc_comment_text(), Some(macro_label(&src.value)), mod_path)
167+
let docs = Documentation::from_ast(&src.value).map(Into::into);
168+
hover_text(docs, Some(macro_label(&src.value)), mod_path)
173169
}
174170
Definition::Field(it) => {
175171
let src = it.source(db);
176172
match src.value {
177173
FieldSource::Named(it) => {
178-
hover_text(it.doc_comment_text(), it.short_label(), mod_path)
174+
let docs = Documentation::from_ast(&it).map(Into::into);
175+
hover_text(docs, it.short_label(), mod_path)
179176
}
180177
_ => None,
181178
}
182179
}
183180
Definition::ModuleDef(it) => match it {
184181
ModuleDef::Module(it) => match it.definition_source(db).value {
185182
ModuleSource::Module(it) => {
186-
hover_text(it.doc_comment_text(), it.short_label(), mod_path)
183+
let docs = Documentation::from_ast(&it).map(Into::into);
184+
hover_text(docs, it.short_label(), mod_path)
187185
}
188186
_ => None,
189187
},
@@ -208,10 +206,11 @@ fn hover_text_from_name_kind(db: &RootDatabase, def: Definition) -> Option<Strin
208206
fn from_def_source<A, D>(db: &RootDatabase, def: D, mod_path: Option<String>) -> Option<String>
209207
where
210208
D: HasSource<Ast = A>,
211-
A: ast::DocCommentsOwner + ast::NameOwner + ShortLabel,
209+
A: ast::DocCommentsOwner + ast::NameOwner + ShortLabel + ast::AttrsOwner,
212210
{
213211
let src = def.source(db);
214-
hover_text(src.value.doc_comment_text(), src.value.short_label(), mod_path)
212+
let docs = Documentation::from_ast(&src.value).map(Into::into);
213+
hover_text(docs, src.value.short_label(), mod_path)
215214
}
216215
}
217216

@@ -951,4 +950,106 @@ fn func(foo: i32) { if true { <|>foo; }; }
951950
&["mod my"],
952951
);
953952
}
953+
954+
#[test]
955+
fn test_hover_struct_doc_comment() {
956+
check_hover_result(
957+
r#"
958+
//- /lib.rs
959+
/// bar docs
960+
struct Bar;
961+
962+
fn foo() {
963+
let bar = Ba<|>r;
964+
}
965+
"#,
966+
&["struct Bar\n```\n___\n\nbar docs"],
967+
);
968+
}
969+
970+
#[test]
971+
fn test_hover_struct_doc_attr() {
972+
check_hover_result(
973+
r#"
974+
//- /lib.rs
975+
#[doc = "bar docs"]
976+
struct Bar;
977+
978+
fn foo() {
979+
let bar = Ba<|>r;
980+
}
981+
"#,
982+
&["struct Bar\n```\n___\n\nbar docs"],
983+
);
984+
}
985+
986+
#[test]
987+
fn test_hover_struct_doc_attr_multiple_and_mixed() {
988+
check_hover_result(
989+
r#"
990+
//- /lib.rs
991+
/// bar docs 0
992+
#[doc = "bar docs 1"]
993+
#[doc = "bar docs 2"]
994+
struct Bar;
995+
996+
fn foo() {
997+
let bar = Ba<|>r;
998+
}
999+
"#,
1000+
&["struct Bar\n```\n___\n\nbar docs 0\n\nbar docs 1\n\nbar docs 2"],
1001+
);
1002+
}
1003+
1004+
#[test]
1005+
fn test_hover_macro_generated_struct_fn_doc_comment() {
1006+
check_hover_result(
1007+
r#"
1008+
//- /lib.rs
1009+
macro_rules! bar {
1010+
() => {
1011+
struct Bar;
1012+
impl Bar {
1013+
/// Do the foo
1014+
fn foo(&self) {}
1015+
}
1016+
}
1017+
}
1018+
1019+
bar!();
1020+
1021+
fn foo() {
1022+
let bar = Bar;
1023+
bar.fo<|>o();
1024+
}
1025+
"#,
1026+
&["Bar\n```\n\n```rust\nfn foo(&self)\n```\n___\n\n Do the foo"],
1027+
);
1028+
}
1029+
1030+
#[test]
1031+
fn test_hover_macro_generated_struct_fn_doc_attr() {
1032+
check_hover_result(
1033+
r#"
1034+
//- /lib.rs
1035+
macro_rules! bar {
1036+
() => {
1037+
struct Bar;
1038+
impl Bar {
1039+
#[doc = "Do the foo"]
1040+
fn foo(&self) {}
1041+
}
1042+
}
1043+
}
1044+
1045+
bar!();
1046+
1047+
fn foo() {
1048+
let bar = Bar;
1049+
bar.fo<|>o();
1050+
}
1051+
"#,
1052+
&["Bar\n```\n\n```rust\nfn foo(&self)\n```\n___\n\nDo the foo"],
1053+
);
1054+
}
9541055
}

crates/ra_syntax/src/ast/traits.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,22 @@ pub trait DocCommentsOwner: AstNode {
8383
CommentIter { iter: self.syntax().children_with_tokens() }
8484
}
8585

86+
fn doc_comment_text(&self) -> Option<String> {
87+
self.doc_comments().doc_comment_text()
88+
}
89+
}
90+
91+
impl CommentIter {
92+
pub fn from_syntax_node(syntax_node: &ast::SyntaxNode) -> CommentIter {
93+
CommentIter { iter: syntax_node.children_with_tokens() }
94+
}
95+
8696
/// Returns the textual content of a doc comment block as a single string.
8797
/// That is, strips leading `///` (+ optional 1 character of whitespace),
8898
/// trailing `*/`, trailing whitespace and then joins the lines.
89-
fn doc_comment_text(&self) -> Option<String> {
99+
pub fn doc_comment_text(self) -> Option<String> {
90100
let mut has_comments = false;
91101
let docs = self
92-
.doc_comments()
93102
.filter(|comment| comment.kind().doc.is_some())
94103
.map(|comment| {
95104
has_comments = true;

0 commit comments

Comments
 (0)