Skip to content

Commit c2be91d

Browse files
bors[bot]Veykril
andauthored
Merge #8245
8245: Properly resolve intra doc links in hover and goto_definition r=matklad a=Veykril Unfortunately involves a bit of weird workarounds due to pulldown_cmark's incorrect lifetimes on `BrokenLinkCallback`... I should probably open an issue there asking for the fixes to be pushed to a release since they already exist in the repo for quite some time it seems. Fixes #8258, Fixes #8238 Co-authored-by: Lukas Wirth <[email protected]>
2 parents d8ee25b + 8d786dc commit c2be91d

File tree

7 files changed

+260
-157
lines changed

7 files changed

+260
-157
lines changed

crates/hir_def/src/attr.rs

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! A higher level attributes based on TokenTree, with also some shortcuts.
22
3-
use std::{ops, sync::Arc};
3+
use std::{
4+
convert::{TryFrom, TryInto},
5+
ops,
6+
sync::Arc,
7+
};
48

59
use base_db::CrateId;
610
use cfg::{CfgExpr, CfgOptions};
@@ -12,7 +16,7 @@ use mbe::ast_to_token_tree;
1216
use smallvec::{smallvec, SmallVec};
1317
use syntax::{
1418
ast::{self, AstNode, AttrsOwner},
15-
match_ast, AstToken, SmolStr, SyntaxNode,
19+
match_ast, AstToken, SmolStr, SyntaxNode, TextRange, TextSize,
1620
};
1721
use tt::Subtree;
1822

@@ -452,6 +456,55 @@ impl AttrsWithOwner {
452456
.collect(),
453457
}
454458
}
459+
460+
pub fn docs_with_rangemap(
461+
&self,
462+
db: &dyn DefDatabase,
463+
) -> Option<(Documentation, DocsRangeMap)> {
464+
// FIXME: code duplication in `docs` above
465+
let docs = self.by_key("doc").attrs().flat_map(|attr| match attr.input.as_ref()? {
466+
AttrInput::Literal(s) => Some((s, attr.index)),
467+
AttrInput::TokenTree(_) => None,
468+
});
469+
let indent = docs
470+
.clone()
471+
.flat_map(|(s, _)| s.lines())
472+
.filter(|line| !line.chars().all(|c| c.is_whitespace()))
473+
.map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
474+
.min()
475+
.unwrap_or(0);
476+
let mut buf = String::new();
477+
let mut mapping = Vec::new();
478+
for (doc, idx) in docs {
479+
// str::lines doesn't yield anything for the empty string
480+
if !doc.is_empty() {
481+
for line in doc.split('\n') {
482+
let line = line.trim_end();
483+
let line_len = line.len();
484+
let (offset, line) = match line.char_indices().nth(indent) {
485+
Some((offset, _)) => (offset, &line[offset..]),
486+
None => (0, line),
487+
};
488+
let buf_offset = buf.len();
489+
buf.push_str(line);
490+
mapping.push((
491+
TextRange::new(buf_offset.try_into().ok()?, buf.len().try_into().ok()?),
492+
idx,
493+
TextRange::new(offset.try_into().ok()?, line_len.try_into().ok()?),
494+
));
495+
buf.push('\n');
496+
}
497+
} else {
498+
buf.push('\n');
499+
}
500+
}
501+
buf.pop();
502+
if buf.is_empty() {
503+
None
504+
} else {
505+
Some((Documentation(buf), DocsRangeMap { mapping, source: self.source_map(db).attrs }))
506+
}
507+
}
455508
}
456509

457510
fn inner_attributes(
@@ -508,6 +561,44 @@ impl AttrSourceMap {
508561
}
509562
}
510563

564+
/// A struct to map text ranges from [`Documentation`] back to TextRanges in the syntax tree.
565+
pub struct DocsRangeMap {
566+
source: Vec<InFile<Either<ast::Attr, ast::Comment>>>,
567+
// (docstring-line-range, attr_index, attr-string-range)
568+
// a mapping from the text range of a line of the [`Documentation`] to the attribute index and
569+
// the original (untrimmed) syntax doc line
570+
mapping: Vec<(TextRange, u32, TextRange)>,
571+
}
572+
573+
impl DocsRangeMap {
574+
pub fn map(&self, range: TextRange) -> Option<InFile<TextRange>> {
575+
let found = self.mapping.binary_search_by(|(probe, ..)| probe.ordering(range)).ok()?;
576+
let (line_docs_range, idx, original_line_src_range) = self.mapping[found].clone();
577+
if !line_docs_range.contains_range(range) {
578+
return None;
579+
}
580+
581+
let relative_range = range - line_docs_range.start();
582+
583+
let &InFile { file_id, value: ref source } = &self.source[idx as usize];
584+
match source {
585+
Either::Left(_) => None, // FIXME, figure out a nice way to handle doc attributes here
586+
// as well as for whats done in syntax highlight doc injection
587+
Either::Right(comment) => {
588+
let text_range = comment.syntax().text_range();
589+
let range = TextRange::at(
590+
text_range.start()
591+
+ TextSize::try_from(comment.prefix().len()).ok()?
592+
+ original_line_src_range.start()
593+
+ relative_range.start(),
594+
text_range.len().min(range.len()),
595+
);
596+
Some(InFile { file_id, value: range })
597+
}
598+
}
599+
}
600+
}
601+
511602
#[derive(Debug, Clone, PartialEq, Eq)]
512603
pub struct Attr {
513604
index: u32,

crates/ide/src/doc_links.rs

Lines changed: 59 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
//! Extracts, resolves and rewrites links and intra-doc links in markdown documentation.
22
3-
use std::{convert::TryFrom, iter::once, ops::Range};
3+
use std::{
4+
convert::{TryFrom, TryInto},
5+
iter::once,
6+
};
47

58
use itertools::Itertools;
69
use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
@@ -16,8 +19,7 @@ use ide_db::{
1619
RootDatabase,
1720
};
1821
use syntax::{
19-
ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TextSize,
20-
TokenAtOffset, T,
22+
ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TokenAtOffset, T,
2123
};
2224

2325
use crate::{FilePosition, Semantics};
@@ -26,12 +28,7 @@ pub(crate) type DocumentationLink = String;
2628

2729
/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
2830
pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
29-
let mut cb = |link: BrokenLink| {
30-
Some((
31-
/*url*/ link.reference.to_owned().into(),
32-
/*title*/ link.reference.to_owned().into(),
33-
))
34-
};
31+
let mut cb = broken_link_clone_cb;
3532
let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
3633

3734
let doc = map_links(doc, |target, title: &str| {
@@ -123,74 +120,27 @@ pub(crate) fn external_docs(
123120
/// Extracts all links from a given markdown text.
124121
pub(crate) fn extract_definitions_from_markdown(
125122
markdown: &str,
126-
) -> Vec<(Range<usize>, String, Option<hir::Namespace>)> {
127-
let mut res = vec![];
128-
let mut cb = |link: BrokenLink| {
129-
// These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong
130-
// this is fixed in the repo but not on the crates.io release yet
131-
Some((
132-
/*url*/ link.reference.to_owned().into(),
133-
/*title*/ link.reference.to_owned().into(),
134-
))
135-
};
136-
let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
137-
for (event, range) in doc.into_offset_iter() {
123+
) -> Vec<(TextRange, String, Option<hir::Namespace>)> {
124+
Parser::new_with_broken_link_callback(
125+
markdown,
126+
Options::empty(),
127+
Some(&mut broken_link_clone_cb),
128+
)
129+
.into_offset_iter()
130+
.filter_map(|(event, range)| {
138131
if let Event::Start(Tag::Link(_, target, title)) = event {
139132
let link = if target.is_empty() { title } else { target };
140133
let (link, ns) = parse_intra_doc_link(&link);
141-
res.push((range, link.to_string(), ns));
142-
}
143-
}
144-
res
145-
}
146-
147-
/// Extracts a link from a comment at the given position returning the spanning range, link and
148-
/// optionally it's namespace.
149-
pub(crate) fn extract_positioned_link_from_comment(
150-
position: TextSize,
151-
comment: &ast::Comment,
152-
) -> Option<(TextRange, String, Option<hir::Namespace>)> {
153-
let doc_comment = comment.doc_comment()?;
154-
let comment_start =
155-
comment.syntax().text_range().start() + TextSize::from(comment.prefix().len() as u32);
156-
let def_links = extract_definitions_from_markdown(doc_comment);
157-
let (range, def_link, ns) =
158-
def_links.into_iter().find_map(|(Range { start, end }, def_link, ns)| {
159-
let range = TextRange::at(
160-
comment_start + TextSize::from(start as u32),
161-
TextSize::from((end - start) as u32),
162-
);
163-
range.contains(position).then(|| (range, def_link, ns))
164-
})?;
165-
Some((range, def_link, ns))
166-
}
167-
168-
/// Turns a syntax node into it's [`Definition`] if it can hold docs.
169-
pub(crate) fn doc_owner_to_def(
170-
sema: &Semantics<RootDatabase>,
171-
item: &SyntaxNode,
172-
) -> Option<Definition> {
173-
let res: hir::ModuleDef = match_ast! {
174-
match item {
175-
ast::SourceFile(_it) => sema.scope(item).module()?.into(),
176-
ast::Fn(it) => sema.to_def(&it)?.into(),
177-
ast::Struct(it) => sema.to_def(&it)?.into(),
178-
ast::Enum(it) => sema.to_def(&it)?.into(),
179-
ast::Union(it) => sema.to_def(&it)?.into(),
180-
ast::Trait(it) => sema.to_def(&it)?.into(),
181-
ast::Const(it) => sema.to_def(&it)?.into(),
182-
ast::Static(it) => sema.to_def(&it)?.into(),
183-
ast::TypeAlias(it) => sema.to_def(&it)?.into(),
184-
ast::Variant(it) => sema.to_def(&it)?.into(),
185-
ast::Trait(it) => sema.to_def(&it)?.into(),
186-
ast::Impl(it) => return sema.to_def(&it).map(Definition::SelfType),
187-
ast::Macro(it) => return sema.to_def(&it).map(Definition::Macro),
188-
ast::TupleField(it) => return sema.to_def(&it).map(Definition::Field),
189-
ast::RecordField(it) => return sema.to_def(&it).map(Definition::Field),
190-
_ => return None,
134+
Some((
135+
TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?),
136+
link.to_string(),
137+
ns,
138+
))
139+
} else {
140+
None
191141
}
192-
};
193-
Some(Definition::ModuleDef(res))
142+
})
143+
.collect()
194144
}
195145

196146
pub(crate) fn resolve_doc_path_for_def(
@@ -220,6 +170,42 @@ pub(crate) fn resolve_doc_path_for_def(
220170
}
221171
}
222172

173+
pub(crate) fn doc_attributes(
174+
sema: &Semantics<RootDatabase>,
175+
node: &SyntaxNode,
176+
) -> Option<(hir::AttrsWithOwner, Definition)> {
177+
match_ast! {
178+
match node {
179+
ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
180+
ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
181+
ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))),
182+
ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))),
183+
ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))),
184+
ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))),
185+
ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))),
186+
ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))),
187+
ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))),
188+
ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))),
189+
ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))),
190+
ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))),
191+
ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
192+
ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
193+
ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))),
194+
// ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
195+
_ => return None
196+
}
197+
}
198+
}
199+
200+
fn broken_link_clone_cb<'a, 'b>(link: BrokenLink<'a>) -> Option<(CowStr<'b>, CowStr<'b>)> {
201+
// These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong
202+
// this is fixed in the repo but not on the crates.io release yet
203+
Some((
204+
/*url*/ link.reference.to_owned().into(),
205+
/*title*/ link.reference.to_owned().into(),
206+
))
207+
}
208+
223209
// FIXME:
224210
// BUG: For Option::Some
225211
// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some

crates/ide/src/goto_definition.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use either::Either;
2-
use hir::Semantics;
2+
use hir::{InFile, Semantics};
33
use ide_db::{
44
defs::{NameClass, NameRefClass},
55
RootDatabase,
@@ -8,7 +8,7 @@ use syntax::{ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxToken, Toke
88

99
use crate::{
1010
display::TryToNav,
11-
doc_links::{doc_owner_to_def, extract_positioned_link_from_comment, resolve_doc_path_for_def},
11+
doc_links::{doc_attributes, extract_definitions_from_markdown, resolve_doc_path_for_def},
1212
FilePosition, NavigationTarget, RangeInfo,
1313
};
1414

@@ -32,9 +32,16 @@ pub(crate) fn goto_definition(
3232
let original_token = pick_best(file.token_at_offset(position.offset))?;
3333
let token = sema.descend_into_macros(original_token.clone());
3434
let parent = token.parent()?;
35-
if let Some(comment) = ast::Comment::cast(token) {
36-
let (_, link, ns) = extract_positioned_link_from_comment(position.offset, &comment)?;
37-
let def = doc_owner_to_def(&sema, &parent)?;
35+
if let Some(_) = ast::Comment::cast(token) {
36+
let (attributes, def) = doc_attributes(&sema, &parent)?;
37+
38+
let (docs, doc_mapping) = attributes.docs_with_rangemap(db)?;
39+
let (_, link, ns) =
40+
extract_definitions_from_markdown(docs.as_str()).into_iter().find(|(range, ..)| {
41+
doc_mapping.map(range.clone()).map_or(false, |InFile { file_id, value: range }| {
42+
file_id == position.file_id.into() && range.contains(position.offset)
43+
})
44+
})?;
3845
let nav = resolve_doc_path_for_def(db, def, &link, ns)?.try_to_nav(db)?;
3946
return Some(RangeInfo::new(original_token.text_range(), vec![nav]));
4047
}
@@ -1160,4 +1167,25 @@ fn fn_macro() {}
11601167
"#,
11611168
)
11621169
}
1170+
1171+
#[test]
1172+
fn goto_intra_doc_links() {
1173+
check(
1174+
r#"
1175+
1176+
pub mod theitem {
1177+
/// This is the item. Cool!
1178+
pub struct TheItem;
1179+
//^^^^^^^
1180+
}
1181+
1182+
/// Gives you a [`TheItem$0`].
1183+
///
1184+
/// [`TheItem`]: theitem::TheItem
1185+
pub fn gimme() -> theitem::TheItem {
1186+
theitem::TheItem
1187+
}
1188+
"#,
1189+
);
1190+
}
11631191
}

0 commit comments

Comments
 (0)