diff options
Diffstat (limited to 'crates/ide/src/doc_links.rs')
-rw-r--r-- | crates/ide/src/doc_links.rs | 260 |
1 files changed, 154 insertions, 106 deletions
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index c7c1f4fee..0cee741ac 100644 --- a/crates/ide/src/doc_links.rs +++ b/crates/ide/src/doc_links.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | //! Resolves and rewrites 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::{convert::TryFrom, iter::once, ops::Range}; |
4 | 4 | ||
@@ -15,7 +15,10 @@ use ide_db::{ | |||
15 | defs::{Definition, NameClass, NameRefClass}, | 15 | defs::{Definition, NameClass, NameRefClass}, |
16 | RootDatabase, | 16 | RootDatabase, |
17 | }; | 17 | }; |
18 | use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T}; | 18 | use syntax::{ |
19 | ast, match_ast, AstNode, AstToken, SyntaxKind::*, SyntaxNode, SyntaxToken, TextRange, TextSize, | ||
20 | TokenAtOffset, T, | ||
21 | }; | ||
19 | 22 | ||
20 | use crate::{FilePosition, Semantics}; | 23 | use crate::{FilePosition, Semantics}; |
21 | 24 | ||
@@ -60,29 +63,6 @@ pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Defi | |||
60 | out | 63 | out |
61 | } | 64 | } |
62 | 65 | ||
63 | pub(crate) fn extract_definitions_from_markdown( | ||
64 | markdown: &str, | ||
65 | ) -> Vec<(String, Option<hir::Namespace>, Range<usize>)> { | ||
66 | let mut res = vec![]; | ||
67 | let mut cb = |link: BrokenLink| { | ||
68 | // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong | ||
69 | // this is fixed in the repo but not on the crates.io release yet | ||
70 | Some(( | ||
71 | /*url*/ link.reference.to_owned().into(), | ||
72 | /*title*/ link.reference.to_owned().into(), | ||
73 | )) | ||
74 | }; | ||
75 | let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb)); | ||
76 | for (event, range) in doc.into_offset_iter() { | ||
77 | if let Event::Start(Tag::Link(_, target, title)) = event { | ||
78 | let link = if target.is_empty() { title } else { target }; | ||
79 | let (link, ns) = parse_link(&link); | ||
80 | res.push((link.to_string(), ns, range)); | ||
81 | } | ||
82 | } | ||
83 | res | ||
84 | } | ||
85 | |||
86 | /// Remove all links in markdown documentation. | 66 | /// Remove all links in markdown documentation. |
87 | pub(crate) fn remove_links(markdown: &str) -> String { | 67 | pub(crate) fn remove_links(markdown: &str) -> String { |
88 | let mut drop_link = false; | 68 | let mut drop_link = false; |
@@ -118,6 +98,105 @@ pub(crate) fn remove_links(markdown: &str) -> String { | |||
118 | out | 98 | out |
119 | } | 99 | } |
120 | 100 | ||
101 | pub(crate) fn extract_definitions_from_markdown( | ||
102 | markdown: &str, | ||
103 | ) -> Vec<(Range<usize>, String, Option<hir::Namespace>)> { | ||
104 | let mut res = vec![]; | ||
105 | let mut cb = |link: BrokenLink| { | ||
106 | // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong | ||
107 | // this is fixed in the repo but not on the crates.io release yet | ||
108 | Some(( | ||
109 | /*url*/ link.reference.to_owned().into(), | ||
110 | /*title*/ link.reference.to_owned().into(), | ||
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 { | ||
116 | let link = if target.is_empty() { title } else { target }; | ||
117 | let (link, ns) = parse_intra_doc_link(&link); | ||
118 | res.push((range, link.to_string(), ns)); | ||
119 | } | ||
120 | } | ||
121 | res | ||
122 | } | ||
123 | |||
124 | /// Extracts a link from a comment at the given position returning the spanning range, link and | ||
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 | } | ||
169 | }; | ||
170 | Some(Definition::ModuleDef(res)) | ||
171 | } | ||
172 | |||
173 | pub(crate) fn resolve_doc_path_for_def( | ||
174 | db: &dyn HirDatabase, | ||
175 | def: Definition, | ||
176 | link: &str, | ||
177 | ns: Option<hir::Namespace>, | ||
178 | ) -> Option<hir::ModuleDef> { | ||
179 | match def { | ||
180 | Definition::ModuleDef(def) => match def { | ||
181 | ModuleDef::Module(it) => it.resolve_doc_path(db, &link, ns), | ||
182 | ModuleDef::Function(it) => it.resolve_doc_path(db, &link, ns), | ||
183 | ModuleDef::Adt(it) => it.resolve_doc_path(db, &link, ns), | ||
184 | ModuleDef::Variant(it) => it.resolve_doc_path(db, &link, ns), | ||
185 | ModuleDef::Const(it) => it.resolve_doc_path(db, &link, ns), | ||
186 | ModuleDef::Static(it) => it.resolve_doc_path(db, &link, ns), | ||
187 | ModuleDef::Trait(it) => it.resolve_doc_path(db, &link, ns), | ||
188 | ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, &link, ns), | ||
189 | ModuleDef::BuiltinType(_) => None, | ||
190 | }, | ||
191 | Definition::Macro(it) => it.resolve_doc_path(db, &link, ns), | ||
192 | Definition::Field(it) => it.resolve_doc_path(db, &link, ns), | ||
193 | Definition::SelfType(_) | ||
194 | | Definition::Local(_) | ||
195 | | Definition::GenericParam(_) | ||
196 | | Definition::Label(_) => None, | ||
197 | } | ||
198 | } | ||
199 | |||
121 | // FIXME: | 200 | // FIXME: |
122 | // BUG: For Option::Some | 201 | // BUG: For Option::Some |
123 | // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some | 202 | // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some |
@@ -197,26 +276,8 @@ fn rewrite_intra_doc_link( | |||
197 | title: &str, | 276 | title: &str, |
198 | ) -> Option<(String, String)> { | 277 | ) -> Option<(String, String)> { |
199 | let link = if target.is_empty() { title } else { target }; | 278 | let link = if target.is_empty() { title } else { target }; |
200 | let (link, ns) = parse_link(link); | 279 | let (link, ns) = parse_intra_doc_link(link); |
201 | let resolved = match def { | 280 | let resolved = resolve_doc_path_for_def(db, def, link, ns)?; |
202 | Definition::ModuleDef(def) => match def { | ||
203 | ModuleDef::Module(it) => it.resolve_doc_path(db, link, ns), | ||
204 | ModuleDef::Function(it) => it.resolve_doc_path(db, link, ns), | ||
205 | ModuleDef::Adt(it) => it.resolve_doc_path(db, link, ns), | ||
206 | ModuleDef::Variant(it) => it.resolve_doc_path(db, link, ns), | ||
207 | ModuleDef::Const(it) => it.resolve_doc_path(db, link, ns), | ||
208 | ModuleDef::Static(it) => it.resolve_doc_path(db, link, ns), | ||
209 | ModuleDef::Trait(it) => it.resolve_doc_path(db, link, ns), | ||
210 | ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, link, ns), | ||
211 | ModuleDef::BuiltinType(_) => return None, | ||
212 | }, | ||
213 | Definition::Macro(it) => it.resolve_doc_path(db, link, ns), | ||
214 | Definition::Field(it) => it.resolve_doc_path(db, link, ns), | ||
215 | Definition::SelfType(_) | ||
216 | | Definition::Local(_) | ||
217 | | Definition::GenericParam(_) | ||
218 | | Definition::Label(_) => return None, | ||
219 | }?; | ||
220 | let krate = resolved.module(db)?.krate(); | 281 | let krate = resolved.module(db)?.krate(); |
221 | let canonical_path = resolved.canonical_path(db)?; | 282 | let canonical_path = resolved.canonical_path(db)?; |
222 | let mut new_url = get_doc_url(db, &krate)? | 283 | let mut new_url = get_doc_url(db, &krate)? |
@@ -228,24 +289,23 @@ fn rewrite_intra_doc_link( | |||
228 | .ok()?; | 289 | .ok()?; |
229 | 290 | ||
230 | if let ModuleDef::Trait(t) = resolved { | 291 | if let ModuleDef::Trait(t) = resolved { |
231 | let items = t.items(db); | 292 | if let Some(assoc_item) = t.items(db).into_iter().find_map(|assoc_item| { |
232 | if let Some(field_or_assoc_item) = items.iter().find_map(|assoc_item| { | ||
233 | if let Some(name) = assoc_item.name(db) { | 293 | if let Some(name) = assoc_item.name(db) { |
234 | if *link == format!("{}::{}", canonical_path, name) { | 294 | if *link == format!("{}::{}", canonical_path, name) { |
235 | return Some(FieldOrAssocItem::AssocItem(*assoc_item)); | 295 | return Some(assoc_item); |
236 | } | 296 | } |
237 | } | 297 | } |
238 | None | 298 | None |
239 | }) { | 299 | }) { |
240 | if let Some(fragment) = get_symbol_fragment(db, &field_or_assoc_item) { | 300 | if let Some(fragment) = |
301 | get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(assoc_item)) | ||
302 | { | ||
241 | new_url = new_url.join(&fragment).ok()?; | 303 | new_url = new_url.join(&fragment).ok()?; |
242 | } | 304 | } |
243 | }; | 305 | }; |
244 | } | 306 | } |
245 | 307 | ||
246 | let new_target = new_url.into_string(); | 308 | Some((new_url.into_string(), strip_prefixes_suffixes(title).to_string())) |
247 | let new_title = strip_prefixes_suffixes(title); | ||
248 | Some((new_target, new_title.to_string())) | ||
249 | } | 309 | } |
250 | 310 | ||
251 | /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). | 311 | /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). |
@@ -322,73 +382,61 @@ fn map_links<'e>( | |||
322 | }) | 382 | }) |
323 | } | 383 | } |
324 | 384 | ||
325 | fn parse_link(s: &str) -> (&str, Option<hir::Namespace>) { | 385 | const TYPES: ([&str; 9], [&str; 0]) = |
326 | let path = strip_prefixes_suffixes(s); | 386 | (["type", "struct", "enum", "mod", "trait", "union", "module", "prim", "primitive"], []); |
327 | let ns = ns_from_intra_spec(s); | 387 | const VALUES: ([&str; 8], [&str; 1]) = |
328 | (path, ns) | ||
329 | } | ||
330 | |||
331 | /// Strip prefixes, suffixes, and inline code marks from the given string. | ||
332 | fn strip_prefixes_suffixes(mut s: &str) -> &str { | ||
333 | s = s.trim_matches('`'); | ||
334 | |||
335 | [ | ||
336 | (TYPES.0.iter(), TYPES.1.iter()), | ||
337 | (VALUES.0.iter(), VALUES.1.iter()), | ||
338 | (MACROS.0.iter(), MACROS.1.iter()), | ||
339 | ] | ||
340 | .iter() | ||
341 | .for_each(|(prefixes, suffixes)| { | ||
342 | prefixes.clone().for_each(|prefix| s = s.trim_start_matches(*prefix)); | ||
343 | suffixes.clone().for_each(|suffix| s = s.trim_end_matches(*suffix)); | ||
344 | }); | ||
345 | s.trim_start_matches('@').trim() | ||
346 | } | ||
347 | |||
348 | static TYPES: ([&str; 7], [&str; 0]) = | ||
349 | (["type", "struct", "enum", "mod", "trait", "union", "module"], []); | ||
350 | static VALUES: ([&str; 8], [&str; 1]) = | ||
351 | (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]); | 388 | (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]); |
352 | static MACROS: ([&str; 1], [&str; 1]) = (["macro"], ["!"]); | 389 | const MACROS: ([&str; 2], [&str; 1]) = (["macro", "derive"], ["!"]); |
353 | 390 | ||
354 | /// Extract the specified namespace from an intra-doc-link if one exists. | 391 | /// Extract the specified namespace from an intra-doc-link if one exists. |
355 | /// | 392 | /// |
356 | /// # Examples | 393 | /// # Examples |
357 | /// | 394 | /// |
358 | /// * `struct MyStruct` -> `Namespace::Types` | 395 | /// * `struct MyStruct` -> ("MyStruct", `Namespace::Types`) |
359 | /// * `panic!` -> `Namespace::Macros` | 396 | /// * `panic!` -> ("panic", `Namespace::Macros`) |
360 | /// * `fn@from_intra_spec` -> `Namespace::Values` | 397 | /// * `fn@from_intra_spec` -> ("from_intra_spec", `Namespace::Values`) |
361 | fn ns_from_intra_spec(s: &str) -> Option<hir::Namespace> { | 398 | fn parse_intra_doc_link(s: &str) -> (&str, Option<hir::Namespace>) { |
399 | let s = s.trim_matches('`'); | ||
400 | |||
362 | [ | 401 | [ |
363 | (hir::Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())), | 402 | (hir::Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())), |
364 | (hir::Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())), | 403 | (hir::Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())), |
365 | (hir::Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())), | 404 | (hir::Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())), |
366 | ] | 405 | ] |
367 | .iter() | 406 | .iter() |
368 | .filter(|(_ns, (prefixes, suffixes))| { | 407 | .cloned() |
369 | prefixes | 408 | .find_map(|(ns, (mut prefixes, mut suffixes))| { |
370 | .clone() | 409 | if let Some(prefix) = prefixes.find(|&&prefix| { |
371 | .map(|prefix| { | 410 | s.starts_with(prefix) |
372 | s.starts_with(*prefix) | 411 | && s.chars().nth(prefix.len()).map_or(false, |c| c == '@' || c == ' ') |
373 | && s.chars() | 412 | }) { |
374 | .nth(prefix.len() + 1) | 413 | Some((&s[prefix.len() + 1..], ns)) |
375 | .map(|c| c == '@' || c == ' ') | 414 | } else { |
376 | .unwrap_or(false) | 415 | suffixes.find_map(|&suffix| s.strip_suffix(suffix).zip(Some(ns))) |
377 | }) | 416 | } |
378 | .any(|cond| cond) | 417 | }) |
379 | || suffixes | 418 | .map_or((s, None), |(s, ns)| (s, Some(ns))) |
380 | .clone() | 419 | } |
381 | .map(|suffix| { | 420 | |
382 | s.starts_with(*suffix) | 421 | fn strip_prefixes_suffixes(s: &str) -> &str { |
383 | && s.chars() | 422 | [ |
384 | .nth(suffix.len() + 1) | 423 | (TYPES.0.iter(), TYPES.1.iter()), |
385 | .map(|c| c == '@' || c == ' ') | 424 | (VALUES.0.iter(), VALUES.1.iter()), |
386 | .unwrap_or(false) | 425 | (MACROS.0.iter(), MACROS.1.iter()), |
387 | }) | 426 | ] |
388 | .any(|cond| cond) | 427 | .iter() |
428 | .cloned() | ||
429 | .find_map(|(mut prefixes, mut suffixes)| { | ||
430 | if let Some(prefix) = prefixes.find(|&&prefix| { | ||
431 | s.starts_with(prefix) | ||
432 | && s.chars().nth(prefix.len()).map_or(false, |c| c == '@' || c == ' ') | ||
433 | }) { | ||
434 | Some(&s[prefix.len() + 1..]) | ||
435 | } else { | ||
436 | suffixes.find_map(|&suffix| s.strip_suffix(suffix)) | ||
437 | } | ||
389 | }) | 438 | }) |
390 | .map(|(ns, (_, _))| *ns) | 439 | .unwrap_or(s) |
391 | .next() | ||
392 | } | 440 | } |
393 | 441 | ||
394 | /// Get the root URL for the documentation of a crate. | 442 | /// Get the root URL for the documentation of a crate. |