aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/doc_links.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src/doc_links.rs')
-rw-r--r--crates/ide/src/doc_links.rs197
1 files changed, 92 insertions, 105 deletions
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs
index 0cee741ac..c5dc14a23 100644
--- a/crates/ide/src/doc_links.rs
+++ b/crates/ide/src/doc_links.rs
@@ -1,6 +1,9 @@
1//! Extracts, resolves and rewrites links and intra-doc links in markdown documentation. 1//! Extracts, resolves and rewrites links and intra-doc links in markdown documentation.
2 2
3use std::{convert::TryFrom, iter::once, ops::Range}; 3use std::{
4 convert::{TryFrom, TryInto},
5 iter::once,
6};
4 7
5use itertools::Itertools; 8use itertools::Itertools;
6use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; 9use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
@@ -16,8 +19,7 @@ use ide_db::{
16 RootDatabase, 19 RootDatabase,
17}; 20};
18use syntax::{ 21use syntax::{
19 ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TextSize, 22 ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TokenAtOffset, T,
20 TokenAtOffset, T,
21}; 23};
22 24
23use crate::{FilePosition, Semantics}; 25use crate::{FilePosition, Semantics};
@@ -26,12 +28,7 @@ pub(crate) type DocumentationLink = String;
26 28
27/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) 29/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
28pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { 30pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
29 let mut cb = |link: BrokenLink| { 31 let mut cb = broken_link_clone_cb;
30 Some((
31 /*url*/ link.reference.to_owned().into(),
32 /*title*/ link.reference.to_owned().into(),
33 ))
34 };
35 let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb)); 32 let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
36 33
37 let doc = map_links(doc, |target, title: &str| { 34 let doc = map_links(doc, |target, title: &str| {
@@ -98,76 +95,52 @@ pub(crate) fn remove_links(markdown: &str) -> String {
98 out 95 out
99} 96}
100 97
98/// Retrieve a link to documentation for the given symbol.
99pub(crate) fn external_docs(
100 db: &RootDatabase,
101 position: &FilePosition,
102) -> Option<DocumentationLink> {
103 let sema = Semantics::new(db);
104 let file = sema.parse(position.file_id).syntax().clone();
105 let token = pick_best(file.token_at_offset(position.offset))?;
106 let token = sema.descend_into_macros(token);
107
108 let node = token.parent()?;
109 let definition = match_ast! {
110 match node {
111 ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced(sema.db)),
112 ast::Name(name) => NameClass::classify(&sema, &name).map(|d| d.referenced_or_defined(sema.db)),
113 _ => None,
114 }
115 };
116
117 get_doc_link(db, definition?)
118}
119
120/// Extracts all links from a given markdown text.
101pub(crate) fn extract_definitions_from_markdown( 121pub(crate) fn extract_definitions_from_markdown(
102 markdown: &str, 122 markdown: &str,
103) -> Vec<(Range<usize>, String, Option<hir::Namespace>)> { 123) -> Vec<(TextRange, String, Option<hir::Namespace>)> {
104 let mut res = vec![]; 124 Parser::new_with_broken_link_callback(
105 let mut cb = |link: BrokenLink| { 125 markdown,
106 // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong 126 Options::empty(),
107 // this is fixed in the repo but not on the crates.io release yet 127 Some(&mut broken_link_clone_cb),
108 Some(( 128 )
109 /*url*/ link.reference.to_owned().into(), 129 .into_offset_iter()
110 /*title*/ link.reference.to_owned().into(), 130 .filter_map(|(event, range)| {
111 ))
112 };
113 let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
114 for (event, range) in doc.into_offset_iter() {
115 if let Event::Start(Tag::Link(_, target, title)) = event { 131 if let Event::Start(Tag::Link(_, target, title)) = event {
116 let link = if target.is_empty() { title } else { target }; 132 let link = if target.is_empty() { title } else { target };
117 let (link, ns) = parse_intra_doc_link(&link); 133 let (link, ns) = parse_intra_doc_link(&link);
118 res.push((range, link.to_string(), ns)); 134 Some((
119 } 135 TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?),
120 } 136 link.to_string(),
121 res 137 ns,
122} 138 ))
123 139 } else {
124/// Extracts a link from a comment at the given position returning the spanning range, link and 140 None
125/// optionally it's namespace.
126pub(crate) fn extract_positioned_link_from_comment(
127 position: TextSize,
128 comment: &ast::Comment,
129) -> Option<(TextRange, String, Option<hir::Namespace>)> {
130 let doc_comment = comment.doc_comment()?;
131 let comment_start =
132 comment.syntax().text_range().start() + TextSize::from(comment.prefix().len() as u32);
133 let def_links = extract_definitions_from_markdown(doc_comment);
134 let (range, def_link, ns) =
135 def_links.into_iter().find_map(|(Range { start, end }, def_link, ns)| {
136 let range = TextRange::at(
137 comment_start + TextSize::from(start as u32),
138 TextSize::from((end - start) as u32),
139 );
140 range.contains(position).then(|| (range, def_link, ns))
141 })?;
142 Some((range, def_link, ns))
143}
144
145/// Turns a syntax node into it's [`Definition`] if it can hold docs.
146pub(crate) fn doc_owner_to_def(
147 sema: &Semantics<RootDatabase>,
148 item: &SyntaxNode,
149) -> Option<Definition> {
150 let res: hir::ModuleDef = match_ast! {
151 match item {
152 ast::SourceFile(_it) => sema.scope(item).module()?.into(),
153 ast::Fn(it) => sema.to_def(&it)?.into(),
154 ast::Struct(it) => sema.to_def(&it)?.into(),
155 ast::Enum(it) => sema.to_def(&it)?.into(),
156 ast::Union(it) => sema.to_def(&it)?.into(),
157 ast::Trait(it) => sema.to_def(&it)?.into(),
158 ast::Const(it) => sema.to_def(&it)?.into(),
159 ast::Static(it) => sema.to_def(&it)?.into(),
160 ast::TypeAlias(it) => sema.to_def(&it)?.into(),
161 ast::Variant(it) => sema.to_def(&it)?.into(),
162 ast::Trait(it) => sema.to_def(&it)?.into(),
163 ast::Impl(it) => return sema.to_def(&it).map(Definition::SelfType),
164 ast::MacroRules(it) => return sema.to_def(&it).map(Definition::Macro),
165 ast::TupleField(it) => return sema.to_def(&it).map(Definition::Field),
166 ast::RecordField(it) => return sema.to_def(&it).map(Definition::Field),
167 _ => return None,
168 } 141 }
169 }; 142 })
170 Some(Definition::ModuleDef(res)) 143 .collect()
171} 144}
172 145
173pub(crate) fn resolve_doc_path_for_def( 146pub(crate) fn resolve_doc_path_for_def(
@@ -178,15 +151,15 @@ pub(crate) fn resolve_doc_path_for_def(
178) -> Option<hir::ModuleDef> { 151) -> Option<hir::ModuleDef> {
179 match def { 152 match def {
180 Definition::ModuleDef(def) => match def { 153 Definition::ModuleDef(def) => match def {
181 ModuleDef::Module(it) => it.resolve_doc_path(db, &link, ns), 154 hir::ModuleDef::Module(it) => it.resolve_doc_path(db, &link, ns),
182 ModuleDef::Function(it) => it.resolve_doc_path(db, &link, ns), 155 hir::ModuleDef::Function(it) => it.resolve_doc_path(db, &link, ns),
183 ModuleDef::Adt(it) => it.resolve_doc_path(db, &link, ns), 156 hir::ModuleDef::Adt(it) => it.resolve_doc_path(db, &link, ns),
184 ModuleDef::Variant(it) => it.resolve_doc_path(db, &link, ns), 157 hir::ModuleDef::Variant(it) => it.resolve_doc_path(db, &link, ns),
185 ModuleDef::Const(it) => it.resolve_doc_path(db, &link, ns), 158 hir::ModuleDef::Const(it) => it.resolve_doc_path(db, &link, ns),
186 ModuleDef::Static(it) => it.resolve_doc_path(db, &link, ns), 159 hir::ModuleDef::Static(it) => it.resolve_doc_path(db, &link, ns),
187 ModuleDef::Trait(it) => it.resolve_doc_path(db, &link, ns), 160 hir::ModuleDef::Trait(it) => it.resolve_doc_path(db, &link, ns),
188 ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, &link, ns), 161 hir::ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, &link, ns),
189 ModuleDef::BuiltinType(_) => None, 162 hir::ModuleDef::BuiltinType(_) => None,
190 }, 163 },
191 Definition::Macro(it) => it.resolve_doc_path(db, &link, ns), 164 Definition::Macro(it) => it.resolve_doc_path(db, &link, ns),
192 Definition::Field(it) => it.resolve_doc_path(db, &link, ns), 165 Definition::Field(it) => it.resolve_doc_path(db, &link, ns),
@@ -197,6 +170,42 @@ pub(crate) fn resolve_doc_path_for_def(
197 } 170 }
198} 171}
199 172
173pub(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
200fn 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
200// FIXME: 209// FIXME:
201// BUG: For Option::Some 210// BUG: For Option::Some
202// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some 211// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
@@ -214,7 +223,7 @@ fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
214 .and_then(|assoc| match assoc.container(db) { 223 .and_then(|assoc| match assoc.container(db) {
215 AssocItemContainer::Trait(t) => Some(t.into()), 224 AssocItemContainer::Trait(t) => Some(t.into()),
216 AssocItemContainer::Impl(impld) => { 225 AssocItemContainer::Impl(impld) => {
217 impld.target_ty(db).as_adt().map(|adt| adt.into()) 226 impld.self_ty(db).as_adt().map(|adt| adt.into())
218 } 227 }
219 }) 228 })
220 .unwrap_or_else(|| f.clone().into()), 229 .unwrap_or_else(|| f.clone().into()),
@@ -328,28 +337,6 @@ fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<S
328 .map(|url| url.into_string()) 337 .map(|url| url.into_string())
329} 338}
330 339
331/// Retrieve a link to documentation for the given symbol.
332pub(crate) fn external_docs(
333 db: &RootDatabase,
334 position: &FilePosition,
335) -> Option<DocumentationLink> {
336 let sema = Semantics::new(db);
337 let file = sema.parse(position.file_id).syntax().clone();
338 let token = pick_best(file.token_at_offset(position.offset))?;
339 let token = sema.descend_into_macros(token);
340
341 let node = token.parent()?;
342 let definition = match_ast! {
343 match node {
344 ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced(sema.db)),
345 ast::Name(name) => NameClass::classify(&sema, &name).map(|d| d.referenced_or_defined(sema.db)),
346 _ => None,
347 }
348 };
349
350 get_doc_link(db, definition?)
351}
352
353/// Rewrites a markdown document, applying 'callback' to each link. 340/// Rewrites a markdown document, applying 'callback' to each link.
354fn map_links<'e>( 341fn map_links<'e>(
355 events: impl Iterator<Item = Event<'e>>, 342 events: impl Iterator<Item = Event<'e>>,