Skip to content

Commit d99adc5

Browse files
committed
Make hover work for intra doc links in macro invocations
1 parent 42eb4ef commit d99adc5

File tree

3 files changed

+139
-24
lines changed

3 files changed

+139
-24
lines changed

crates/hir_def/src/attr.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use la_arena::ArenaMap;
1717
use mbe::{syntax_node_to_token_tree, DelimiterKind};
1818
use smallvec::{smallvec, SmallVec};
1919
use syntax::{
20-
ast::{self, AstNode, AttrsOwner},
20+
ast::{self, AstNode, AttrsOwner, IsString},
2121
match_ast, AstPtr, AstToken, SmolStr, SyntaxNode, TextRange, TextSize,
2222
};
2323
use tt::Subtree;
@@ -610,6 +610,7 @@ pub struct DocsRangeMap {
610610
}
611611

612612
impl DocsRangeMap {
613+
/// Maps a [`TextRange`] relative to the documentation string back to its AST range
613614
pub fn map(&self, range: TextRange) -> Option<InFile<TextRange>> {
614615
let found = self.mapping.binary_search_by(|(probe, ..)| probe.ordering(range)).ok()?;
615616
let (line_docs_range, idx, original_line_src_range) = self.mapping[found];
@@ -621,8 +622,15 @@ impl DocsRangeMap {
621622

622623
let InFile { file_id, value: source } = self.source_map.source_of_id(idx);
623624
match source {
624-
Either::Left(_) => None, // FIXME, figure out a nice way to handle doc attributes here
625-
// as well as for whats done in syntax highlight doc injection
625+
Either::Left(attr) => {
626+
let string = get_doc_string_in_attr(&attr)?;
627+
let text_range = string.open_quote_text_range()?;
628+
let range = TextRange::at(
629+
text_range.end() + original_line_src_range.start() + relative_range.start(),
630+
string.syntax().text_range().len().min(range.len()),
631+
);
632+
Some(InFile { file_id, value: range })
633+
}
626634
Either::Right(comment) => {
627635
let text_range = comment.syntax().text_range();
628636
let range = TextRange::at(
@@ -638,6 +646,22 @@ impl DocsRangeMap {
638646
}
639647
}
640648

649+
fn get_doc_string_in_attr(it: &ast::Attr) -> Option<ast::String> {
650+
match it.expr() {
651+
// #[doc = lit]
652+
Some(ast::Expr::Literal(lit)) => match lit.kind() {
653+
ast::LiteralKind::String(it) => Some(it),
654+
_ => None,
655+
},
656+
// #[cfg_attr(..., doc = "", ...)]
657+
None => {
658+
// FIXME: See highlight injection for what to do here
659+
None
660+
}
661+
_ => None,
662+
}
663+
}
664+
641665
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
642666
pub(crate) struct AttrId {
643667
is_doc_comment: bool,

crates/ide/src/hover.rs

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::iter;
1+
use std::{convert::TryFrom, iter};
22

33
use either::Either;
44
use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics, TypeInfo};
@@ -14,8 +14,12 @@ use ide_db::{
1414
use itertools::Itertools;
1515
use stdx::format_to;
1616
use syntax::{
17-
algo, ast, display::fn_as_proc_macro_label, match_ast, AstNode, Direction, SyntaxKind::*,
18-
SyntaxNode, SyntaxToken, T,
17+
algo,
18+
ast::{self, IsString},
19+
display::fn_as_proc_macro_label,
20+
match_ast, AstNode, AstToken, Direction,
21+
SyntaxKind::*,
22+
SyntaxNode, SyntaxToken, TextSize, T,
1923
};
2024

2125
use crate::{
@@ -115,36 +119,53 @@ pub(crate) fn hover(
115119
})?;
116120

117121
let descended = sema.descend_into_macros_many(original_token.clone());
118-
119-
// FIXME handle doc attributes? TokenMap currently doesn't work with comments
120-
if original_token.kind() == COMMENT {
121-
let relative_comment_offset = offset - original_token.text_range().start();
122-
// intra-doc links
122+
// magic intra doc link handling
123+
// FIXME: Lift this out to some other place, goto def wants this as well
124+
let comment_prefix_len = match_ast! {
125+
match original_token {
126+
ast::Comment(comment) => TextSize::try_from(comment.prefix().len()).ok(),
127+
ast::String(string) => original_token.ancestors().find_map(ast::Attr::cast)
128+
.filter(|attr| attr.simple_name().as_deref() == Some("doc")).and_then(|_| string.open_quote_text_range().map(|it| it.len())),
129+
_ => None,
130+
}
131+
};
132+
if let Some(prefix_len) = comment_prefix_len {
123133
cov_mark::hit!(no_highlight_on_comment_hover);
134+
135+
// offset relative to the comments contents
136+
let original_start = original_token.text_range().start();
137+
let relative_comment_offset = offset - original_start - prefix_len;
138+
124139
return descended.iter().find_map(|t| {
125-
match t.kind() {
126-
COMMENT => (),
127-
TOKEN_TREE => {}
128-
_ => return None,
129-
}
130-
let node = t.parent()?;
131-
let absolute_comment_offset = t.text_range().start() + relative_comment_offset;
140+
let (node, descended_prefix_len) = match_ast! {
141+
match t {
142+
ast::Comment(comment) => (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?),
143+
ast::String(string) => (t.ancestors().skip_while(|n| n.kind() != ATTR).nth(1)?, string.open_quote_text_range()?.len()),
144+
_ => return None,
145+
}
146+
};
147+
let token_start = t.text_range().start();
148+
let abs_in_expansion_offset = token_start + relative_comment_offset + descended_prefix_len;
149+
132150
let (attributes, def) = doc_attributes(sema, &node)?;
133151
let (docs, doc_mapping) = attributes.docs_with_rangemap(sema.db)?;
134-
let (idl_range, link, ns) = extract_definitions_from_docs(&docs).into_iter().find_map(
152+
let (in_expansion_range, link, ns) = extract_definitions_from_docs(&docs).into_iter().find_map(
135153
|(range, link, ns)| {
136154
let mapped = doc_mapping.map(range)?;
137-
(mapped.file_id == file_id.into()
138-
&& mapped.value.contains(absolute_comment_offset))
139-
.then(|| (mapped.value, link, ns))
155+
(mapped.value.contains(abs_in_expansion_offset))
156+
.then(|| (mapped.value, link, ns))
140157
},
141158
)?;
159+
// get the relative range to the doc/attribute in the expansion
160+
let in_expansion_relative_range = in_expansion_range - descended_prefix_len - token_start;
161+
// Apply relative range to the original input comment
162+
let absolute_range = in_expansion_relative_range + original_start + prefix_len;
142163
let def = match resolve_doc_path_for_def(sema.db, def, &link, ns)? {
143164
Either::Left(it) => Definition::ModuleDef(it),
144165
Either::Right(it) => Definition::Macro(it),
145166
};
146167
let res = hover_for_definition(sema, file_id, def, &node, config)?;
147-
Some(RangeInfo::new(idl_range, res))
168+
Some(RangeInfo::new(absolute_range, res))
148169
});
149170
}
150171

@@ -4941,4 +4962,63 @@ fn foo() {
49414962
"#]],
49424963
);
49434964
}
4965+
4966+
#[test]
4967+
fn hover_intra_in_macro() {
4968+
check(
4969+
r#"
4970+
macro_rules! foo_macro {
4971+
($(#[$attr:meta])* $name:ident) => {
4972+
$(#[$attr])*
4973+
pub struct $name;
4974+
}
4975+
}
4976+
4977+
foo_macro!(
4978+
/// Doc comment for [`Foo$0`]
4979+
Foo
4980+
);
4981+
"#,
4982+
expect![[r#"
4983+
*[`Foo`]*
4984+
4985+
```rust
4986+
test
4987+
```
4988+
4989+
```rust
4990+
pub struct Foo
4991+
```
4992+
4993+
---
4994+
4995+
Doc comment for [`Foo`](https://docs.rs/test/*/test/struct.Foo.html)
4996+
"#]],
4997+
);
4998+
}
4999+
5000+
#[test]
5001+
fn hover_intra_in_attr() {
5002+
check(
5003+
r#"
5004+
#[doc = "Doc comment for [`Foo$0`]"]
5005+
pub struct Foo;
5006+
"#,
5007+
expect![[r#"
5008+
*[`Foo`]*
5009+
5010+
```rust
5011+
test
5012+
```
5013+
5014+
```rust
5015+
pub struct Foo
5016+
```
5017+
5018+
---
5019+
5020+
Doc comment for [`Foo`](https://docs.rs/test/*/test/struct.Foo.html)
5021+
"#]],
5022+
);
5023+
}
49445024
}

crates/mbe/src/syntax_bridge.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,18 @@ fn convert_tokens<C: TokenConvertor>(conv: &mut C) -> tt::Subtree {
149149
let k: SyntaxKind = token.kind();
150150
if k == COMMENT {
151151
if let Some(tokens) = conv.convert_doc_comment(&token) {
152-
result.extend(tokens);
152+
// FIXME: There has to be a better way to do this
153+
// Add the comments token id to the converted doc string
154+
let id = conv.id_alloc().alloc(range);
155+
result.extend(tokens.into_iter().map(|mut tt| {
156+
if let tt::TokenTree::Subtree(sub) = &mut tt {
157+
if let tt::TokenTree::Leaf(tt::Leaf::Literal(lit)) = &mut sub.token_trees[2]
158+
{
159+
lit.id = id
160+
}
161+
}
162+
tt
163+
}));
153164
}
154165
continue;
155166
}

0 commit comments

Comments
 (0)