]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/doc_links.rs
Add flyimport completion for trait assoc items
[rust.git] / crates / ide / src / doc_links.rs
1 //! Resolves and rewrites links in markdown documentation.
2
3 use std::{convert::TryFrom, iter::once, ops::Range};
4
5 use itertools::Itertools;
6 use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
7 use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
8 use url::Url;
9
10 use hir::{
11     db::{DefDatabase, HirDatabase},
12     Adt, AsAssocItem, AsName, AssocItem, AssocItemContainer, Crate, Field, HasAttrs, ItemInNs,
13     ModuleDef,
14 };
15 use ide_db::{
16     defs::{Definition, NameClass, NameRefClass},
17     RootDatabase,
18 };
19 use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
20
21 use crate::{FilePosition, Semantics};
22
23 pub(crate) type DocumentationLink = String;
24
25 /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
26 pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
27     let mut cb = |link: BrokenLink| {
28         Some((
29             /*url*/ link.reference.to_owned().into(),
30             /*title*/ link.reference.to_owned().into(),
31         ))
32     };
33     let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
34
35     let doc = map_links(doc, |target, title: &str| {
36         // This check is imperfect, there's some overlap between valid intra-doc links
37         // and valid URLs so we choose to be too eager to try to resolve what might be
38         // a URL.
39         if target.contains("://") {
40             (target.to_string(), title.to_string())
41         } else {
42             // Two possibilities:
43             // * path-based links: `../../module/struct.MyStruct.html`
44             // * module-based links (AKA intra-doc links): `super::super::module::MyStruct`
45             if let Some(rewritten) = rewrite_intra_doc_link(db, *definition, target, title) {
46                 return rewritten;
47             }
48             if let Definition::ModuleDef(def) = *definition {
49                 if let Some(target) = rewrite_url_link(db, def, target) {
50                     return (target, title.to_string());
51                 }
52             }
53
54             (target.to_string(), title.to_string())
55         }
56     });
57     let mut out = String::new();
58     let mut options = CmarkOptions::default();
59     options.code_block_backticks = 3;
60     cmark_with_options(doc, &mut out, None, options).ok();
61     out
62 }
63
64 pub(crate) fn extract_definitions_from_markdown(
65     markdown: &str,
66 ) -> Vec<(String, Option<hir::Namespace>, Range<usize>)> {
67     let mut res = vec![];
68     let mut cb = |link: BrokenLink| {
69         Some((
70             /*url*/ link.reference.to_owned().into(),
71             /*title*/ link.reference.to_owned().into(),
72         ))
73     };
74     let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
75     for (event, range) in doc.into_offset_iter() {
76         match event {
77             Event::Start(Tag::Link(_link_type, ref target, ref title)) => {
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         }
84     }
85     res
86 }
87
88 /// Remove all links in markdown documentation.
89 pub(crate) fn remove_links(markdown: &str) -> String {
90     let mut drop_link = false;
91
92     let mut opts = Options::empty();
93     opts.insert(Options::ENABLE_FOOTNOTES);
94
95     let mut cb = |_: BrokenLink| {
96         let empty = InlineStr::try_from("").unwrap();
97         Some((CowStr::Inlined(empty.clone()), CowStr::Inlined(empty)))
98     };
99     let doc = Parser::new_with_broken_link_callback(markdown, opts, Some(&mut cb));
100     let doc = doc.filter_map(move |evt| match evt {
101         Event::Start(Tag::Link(link_type, ref target, ref title)) => {
102             if link_type == LinkType::Inline && target.contains("://") {
103                 Some(Event::Start(Tag::Link(link_type, target.clone(), title.clone())))
104             } else {
105                 drop_link = true;
106                 None
107             }
108         }
109         Event::End(_) if drop_link => {
110             drop_link = false;
111             None
112         }
113         _ => Some(evt),
114     });
115
116     let mut out = String::new();
117     let mut options = CmarkOptions::default();
118     options.code_block_backticks = 3;
119     cmark_with_options(doc, &mut out, None, options).ok();
120     out
121 }
122
123 // FIXME:
124 // BUG: For Option::Some
125 // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
126 // Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
127 //
128 // This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
129 // https://github.com/rust-lang/rfcs/pull/2988
130 fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
131     // Get the outermost definition for the moduledef. This is used to resolve the public path to the type,
132     // then we can join the method, field, etc onto it if required.
133     let target_def: ModuleDef = match definition {
134         Definition::ModuleDef(moddef) => match moddef {
135             ModuleDef::Function(f) => f
136                 .as_assoc_item(db)
137                 .and_then(|assoc| match assoc.container(db) {
138                     AssocItemContainer::Trait(t) => Some(t.into()),
139                     AssocItemContainer::Impl(impld) => {
140                         impld.target_ty(db).as_adt().map(|adt| adt.into())
141                     }
142                 })
143                 .unwrap_or_else(|| f.clone().into()),
144             moddef => moddef,
145         },
146         Definition::Field(f) => f.parent_def(db).into(),
147         // FIXME: Handle macros
148         _ => return None,
149     };
150
151     let ns = ItemInNs::from(target_def.clone());
152
153     let module = definition.module(db)?;
154     let krate = module.krate();
155     let import_map = db.import_map(krate.into());
156     let base = once(krate.display_name(db)?.to_string())
157         .chain(import_map.path_of(ns)?.segments.iter().map(|name| name.to_string()))
158         .join("/")
159         + "/";
160
161     let filename = get_symbol_filename(db, &target_def);
162     let fragment = match definition {
163         Definition::ModuleDef(moddef) => match moddef {
164             ModuleDef::Function(f) => {
165                 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Function(f)))
166             }
167             ModuleDef::Const(c) => {
168                 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Const(c)))
169             }
170             ModuleDef::TypeAlias(ty) => {
171                 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::TypeAlias(ty)))
172             }
173             _ => None,
174         },
175         Definition::Field(field) => get_symbol_fragment(db, &FieldOrAssocItem::Field(field)),
176         _ => None,
177     };
178
179     get_doc_url(db, &krate)?
180         .join(&base)
181         .ok()
182         .and_then(|mut url| {
183             if !matches!(definition, Definition::ModuleDef(ModuleDef::Module(..))) {
184                 url.path_segments_mut().ok()?.pop();
185             };
186             Some(url)
187         })
188         .and_then(|url| url.join(filename.as_deref()?).ok())
189         .and_then(
190             |url| if let Some(fragment) = fragment { url.join(&fragment).ok() } else { Some(url) },
191         )
192         .map(|url| url.into_string())
193 }
194
195 fn rewrite_intra_doc_link(
196     db: &RootDatabase,
197     def: Definition,
198     target: &str,
199     title: &str,
200 ) -> Option<(String, String)> {
201     let link = if target.is_empty() { title } else { target };
202     let (link, ns) = parse_link(link);
203     let resolved = match def {
204         Definition::ModuleDef(def) => match def {
205             ModuleDef::Module(it) => it.resolve_doc_path(db, link, ns),
206             ModuleDef::Function(it) => it.resolve_doc_path(db, link, ns),
207             ModuleDef::Adt(it) => it.resolve_doc_path(db, link, ns),
208             ModuleDef::Variant(it) => it.resolve_doc_path(db, link, ns),
209             ModuleDef::Const(it) => it.resolve_doc_path(db, link, ns),
210             ModuleDef::Static(it) => it.resolve_doc_path(db, link, ns),
211             ModuleDef::Trait(it) => it.resolve_doc_path(db, link, ns),
212             ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, link, ns),
213             ModuleDef::BuiltinType(_) => return None,
214         },
215         Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
216         Definition::Field(it) => it.resolve_doc_path(db, link, ns),
217         Definition::SelfType(_)
218         | Definition::Local(_)
219         | Definition::GenericParam(_)
220         | Definition::Label(_) => return None,
221     }?;
222     let krate = resolved.module(db)?.krate();
223     let canonical_path = resolved.canonical_path(db)?;
224     let new_target = get_doc_url(db, &krate)?
225         .join(&format!("{}/", krate.display_name(db)?))
226         .ok()?
227         .join(&canonical_path.replace("::", "/"))
228         .ok()?
229         .join(&get_symbol_filename(db, &resolved)?)
230         .ok()?
231         .into_string();
232     let new_title = strip_prefixes_suffixes(title);
233     Some((new_target, new_title.to_string()))
234 }
235
236 /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
237 fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<String> {
238     if !(target.contains('#') || target.contains(".html")) {
239         return None;
240     }
241
242     let module = def.module(db)?;
243     let krate = module.krate();
244     let canonical_path = def.canonical_path(db)?;
245     let base = format!("{}/{}", krate.display_name(db)?, canonical_path.replace("::", "/"));
246
247     get_doc_url(db, &krate)
248         .and_then(|url| url.join(&base).ok())
249         .and_then(|url| {
250             get_symbol_filename(db, &def).as_deref().map(|f| url.join(f).ok()).flatten()
251         })
252         .and_then(|url| url.join(target).ok())
253         .map(|url| url.into_string())
254 }
255
256 /// Retrieve a link to documentation for the given symbol.
257 pub(crate) fn external_docs(
258     db: &RootDatabase,
259     position: &FilePosition,
260 ) -> Option<DocumentationLink> {
261     let sema = Semantics::new(db);
262     let file = sema.parse(position.file_id).syntax().clone();
263     let token = pick_best(file.token_at_offset(position.offset))?;
264     let token = sema.descend_into_macros(token);
265
266     let node = token.parent();
267     let definition = match_ast! {
268         match node {
269             ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced(sema.db)),
270             ast::Name(name) => NameClass::classify(&sema, &name).map(|d| d.referenced_or_defined(sema.db)),
271             _ => None,
272         }
273     };
274
275     get_doc_link(db, definition?)
276 }
277
278 /// Rewrites a markdown document, applying 'callback' to each link.
279 fn map_links<'e>(
280     events: impl Iterator<Item = Event<'e>>,
281     callback: impl Fn(&str, &str) -> (String, String),
282 ) -> impl Iterator<Item = Event<'e>> {
283     let mut in_link = false;
284     let mut link_target: Option<CowStr> = None;
285
286     events.map(move |evt| match evt {
287         Event::Start(Tag::Link(_link_type, ref target, _)) => {
288             in_link = true;
289             link_target = Some(target.clone());
290             evt
291         }
292         Event::End(Tag::Link(link_type, _target, _)) => {
293             in_link = false;
294             Event::End(Tag::Link(link_type, link_target.take().unwrap(), CowStr::Borrowed("")))
295         }
296         Event::Text(s) if in_link => {
297             let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
298             link_target = Some(CowStr::Boxed(link_target_s.into()));
299             Event::Text(CowStr::Boxed(link_name.into()))
300         }
301         Event::Code(s) if in_link => {
302             let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
303             link_target = Some(CowStr::Boxed(link_target_s.into()));
304             Event::Code(CowStr::Boxed(link_name.into()))
305         }
306         _ => evt,
307     })
308 }
309
310 fn parse_link(s: &str) -> (&str, Option<hir::Namespace>) {
311     let path = strip_prefixes_suffixes(s);
312     let ns = ns_from_intra_spec(s);
313     (path, ns)
314 }
315
316 /// Strip prefixes, suffixes, and inline code marks from the given string.
317 fn strip_prefixes_suffixes(mut s: &str) -> &str {
318     s = s.trim_matches('`');
319
320     [
321         (TYPES.0.iter(), TYPES.1.iter()),
322         (VALUES.0.iter(), VALUES.1.iter()),
323         (MACROS.0.iter(), MACROS.1.iter()),
324     ]
325     .iter()
326     .for_each(|(prefixes, suffixes)| {
327         prefixes.clone().for_each(|prefix| s = s.trim_start_matches(*prefix));
328         suffixes.clone().for_each(|suffix| s = s.trim_end_matches(*suffix));
329     });
330     s.trim_start_matches('@').trim()
331 }
332
333 static TYPES: ([&str; 7], [&str; 0]) =
334     (["type", "struct", "enum", "mod", "trait", "union", "module"], []);
335 static VALUES: ([&str; 8], [&str; 1]) =
336     (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]);
337 static MACROS: ([&str; 1], [&str; 1]) = (["macro"], ["!"]);
338
339 /// Extract the specified namespace from an intra-doc-link if one exists.
340 ///
341 /// # Examples
342 ///
343 /// * `struct MyStruct` -> `Namespace::Types`
344 /// * `panic!` -> `Namespace::Macros`
345 /// * `fn@from_intra_spec` -> `Namespace::Values`
346 fn ns_from_intra_spec(s: &str) -> Option<hir::Namespace> {
347     [
348         (hir::Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())),
349         (hir::Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())),
350         (hir::Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())),
351     ]
352     .iter()
353     .filter(|(_ns, (prefixes, suffixes))| {
354         prefixes
355             .clone()
356             .map(|prefix| {
357                 s.starts_with(*prefix)
358                     && s.chars()
359                         .nth(prefix.len() + 1)
360                         .map(|c| c == '@' || c == ' ')
361                         .unwrap_or(false)
362             })
363             .any(|cond| cond)
364             || suffixes
365                 .clone()
366                 .map(|suffix| {
367                     s.starts_with(*suffix)
368                         && s.chars()
369                             .nth(suffix.len() + 1)
370                             .map(|c| c == '@' || c == ' ')
371                             .unwrap_or(false)
372                 })
373                 .any(|cond| cond)
374     })
375     .map(|(ns, (_, _))| *ns)
376     .next()
377 }
378
379 /// Get the root URL for the documentation of a crate.
380 ///
381 /// ```
382 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
383 /// ^^^^^^^^^^^^^^^^^^^^^^^^^^
384 /// ```
385 fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
386     krate
387         .get_html_root_url(db)
388         .or_else(|| {
389             // Fallback to docs.rs. This uses `display_name` and can never be
390             // correct, but that's what fallbacks are about.
391             //
392             // FIXME: clicking on the link should just open the file in the editor,
393             // instead of falling back to external urls.
394             Some(format!("https://docs.rs/{}/*/", krate.display_name(db)?))
395         })
396         .and_then(|s| Url::parse(&s).ok())
397 }
398
399 /// Get the filename and extension generated for a symbol by rustdoc.
400 ///
401 /// ```
402 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
403 ///                                    ^^^^^^^^^^^^^^^^^^^
404 /// ```
405 fn get_symbol_filename(db: &dyn HirDatabase, definition: &ModuleDef) -> Option<String> {
406     Some(match definition {
407         ModuleDef::Adt(adt) => match adt {
408             Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
409             Adt::Enum(e) => format!("enum.{}.html", e.name(db)),
410             Adt::Union(u) => format!("union.{}.html", u.name(db)),
411         },
412         ModuleDef::Module(_) => "index.html".to_string(),
413         ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
414         ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
415         ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.as_name()),
416         ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
417         ModuleDef::Variant(ev) => {
418             format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
419         }
420         ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?),
421         ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
422     })
423 }
424
425 enum FieldOrAssocItem {
426     Field(Field),
427     AssocItem(AssocItem),
428 }
429
430 /// Get the fragment required to link to a specific field, method, associated type, or associated constant.
431 ///
432 /// ```
433 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
434 ///                                                       ^^^^^^^^^^^^^^
435 /// ```
436 fn get_symbol_fragment(db: &dyn HirDatabase, field_or_assoc: &FieldOrAssocItem) -> Option<String> {
437     Some(match field_or_assoc {
438         FieldOrAssocItem::Field(field) => format!("#structfield.{}", field.name(db)),
439         FieldOrAssocItem::AssocItem(assoc) => match assoc {
440             AssocItem::Function(function) => {
441                 let is_trait_method = function
442                     .as_assoc_item(db)
443                     .and_then(|assoc| assoc.containing_trait(db))
444                     .is_some();
445                 // This distinction may get more complicated when specialization is available.
446                 // Rustdoc makes this decision based on whether a method 'has defaultness'.
447                 // Currently this is only the case for provided trait methods.
448                 if is_trait_method && !function.has_body(db) {
449                     format!("#tymethod.{}", function.name(db))
450                 } else {
451                     format!("#method.{}", function.name(db))
452                 }
453             }
454             AssocItem::Const(constant) => format!("#associatedconstant.{}", constant.name(db)?),
455             AssocItem::TypeAlias(ty) => format!("#associatedtype.{}", ty.name(db)),
456         },
457     })
458 }
459
460 fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
461     return tokens.max_by_key(priority);
462     fn priority(n: &SyntaxToken) -> usize {
463         match n.kind() {
464             IDENT | INT_NUMBER => 3,
465             T!['('] | T![')'] => 2,
466             kind if kind.is_trivia() => 0,
467             _ => 1,
468         }
469     }
470 }
471
472 #[cfg(test)]
473 mod tests {
474     use expect_test::{expect, Expect};
475
476     use crate::fixture;
477
478     fn check(ra_fixture: &str, expect: Expect) {
479         let (analysis, position) = fixture::position(ra_fixture);
480         let url = analysis.external_docs(position).unwrap().expect("could not find url for symbol");
481
482         expect.assert_eq(&url)
483     }
484
485     #[test]
486     fn test_doc_url_struct() {
487         check(
488             r#"
489 pub struct Fo$0o;
490 "#,
491             expect![[r#"https://docs.rs/test/*/test/struct.Foo.html"#]],
492         );
493     }
494
495     #[test]
496     fn test_doc_url_fn() {
497         check(
498             r#"
499 pub fn fo$0o() {}
500 "#,
501             expect![[r##"https://docs.rs/test/*/test/fn.foo.html#method.foo"##]],
502         );
503     }
504
505     #[test]
506     fn test_doc_url_inherent_method() {
507         check(
508             r#"
509 pub struct Foo;
510
511 impl Foo {
512     pub fn met$0hod() {}
513 }
514
515 "#,
516             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#method.method"##]],
517         );
518     }
519
520     #[test]
521     fn test_doc_url_trait_provided_method() {
522         check(
523             r#"
524 pub trait Bar {
525     fn met$0hod() {}
526 }
527
528 "#,
529             expect![[r##"https://docs.rs/test/*/test/trait.Bar.html#method.method"##]],
530         );
531     }
532
533     #[test]
534     fn test_doc_url_trait_required_method() {
535         check(
536             r#"
537 pub trait Foo {
538     fn met$0hod();
539 }
540
541 "#,
542             expect![[r##"https://docs.rs/test/*/test/trait.Foo.html#tymethod.method"##]],
543         );
544     }
545
546     #[test]
547     fn test_doc_url_field() {
548         check(
549             r#"
550 pub struct Foo {
551     pub fie$0ld: ()
552 }
553
554 "#,
555             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#structfield.field"##]],
556         );
557     }
558
559     #[test]
560     fn test_module() {
561         check(
562             r#"
563 pub mod foo {
564     pub mod ba$0r {}
565 }
566         "#,
567             expect![[r#"https://docs.rs/test/*/test/foo/bar/index.html"#]],
568         )
569     }
570
571     // FIXME: ImportMap will return re-export paths instead of public module
572     // paths. The correct path to documentation will never be a re-export.
573     // This problem stops us from resolving stdlib items included in the prelude
574     // such as `Option::Some` correctly.
575     #[ignore = "ImportMap may return re-exports"]
576     #[test]
577     fn test_reexport_order() {
578         check(
579             r#"
580 pub mod wrapper {
581     pub use module::Item;
582
583     pub mod module {
584         pub struct Item;
585     }
586 }
587
588 fn foo() {
589     let bar: wrapper::It$0em;
590 }
591         "#,
592             expect![[r#"https://docs.rs/test/*/test/wrapper/module/struct.Item.html"#]],
593         )
594     }
595 }