diff options
Diffstat (limited to 'crates/ide/src/doc_links.rs')
-rw-r--r-- | crates/ide/src/doc_links.rs | 197 |
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 | ||
3 | use std::{convert::TryFrom, iter::once, ops::Range}; | 3 | use std::{ |
4 | convert::{TryFrom, TryInto}, | ||
5 | iter::once, | ||
6 | }; | ||
4 | 7 | ||
5 | use itertools::Itertools; | 8 | use itertools::Itertools; |
6 | use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; | 9 | use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; |
@@ -16,8 +19,7 @@ use ide_db::{ | |||
16 | RootDatabase, | 19 | RootDatabase, |
17 | }; | 20 | }; |
18 | use syntax::{ | 21 | use 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 | ||
23 | use crate::{FilePosition, Semantics}; | 25 | use 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) |
28 | pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { | 30 | pub(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. | ||
99 | pub(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. | ||
101 | pub(crate) fn extract_definitions_from_markdown( | 121 | pub(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. | ||
126 | pub(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. | ||
146 | pub(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 | ||
173 | pub(crate) fn resolve_doc_path_for_def( | 146 | pub(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 | ||
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 | |||
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. | ||
332 | pub(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. |
354 | fn map_links<'e>( | 341 | fn map_links<'e>( |
355 | events: impl Iterator<Item = Event<'e>>, | 342 | events: impl Iterator<Item = Event<'e>>, |