]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/doc_links.rs
41557ca1d9d9d53e4c68444032cdb8de18c8ede4
[rust.git] / crates / ide / src / doc_links.rs
1 //! Extracts, resolves and rewrites links and intra-doc links in markdown documentation.
2
3 mod intra_doc_links;
4
5 use std::convert::{TryFrom, TryInto};
6
7 use either::Either;
8 use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
9 use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
10 use stdx::format_to;
11 use url::Url;
12
13 use hir::{
14     db::HirDatabase, Adt, AsAssocItem, AssocItem, AssocItemContainer, Crate, HasAttrs, MacroDef,
15     ModuleDef,
16 };
17 use ide_db::{
18     defs::{Definition, NameClass, NameRefClass},
19     helpers::pick_best_token,
20     RootDatabase,
21 };
22 use syntax::{
23     ast::{self, IsString},
24     match_ast, AstNode, AstToken,
25     SyntaxKind::*,
26     SyntaxNode, SyntaxToken, TextRange, TextSize, T,
27 };
28
29 use crate::{
30     doc_links::intra_doc_links::{parse_intra_doc_link, strip_prefixes_suffixes},
31     FilePosition, Semantics,
32 };
33
34 /// Weblink to an item's documentation.
35 pub(crate) type DocumentationLink = String;
36
37 /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
38 pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: Definition) -> String {
39     let mut cb = broken_link_clone_cb;
40     let doc =
41         Parser::new_with_broken_link_callback(markdown, Options::ENABLE_TASKLISTS, Some(&mut cb));
42
43     let doc = map_links(doc, |target, title| {
44         // This check is imperfect, there's some overlap between valid intra-doc links
45         // and valid URLs so we choose to be too eager to try to resolve what might be
46         // a URL.
47         if target.contains("://") {
48             (target.to_string(), title.to_string())
49         } else {
50             // Two possibilities:
51             // * path-based links: `../../module/struct.MyStruct.html`
52             // * module-based links (AKA intra-doc links): `super::super::module::MyStruct`
53             if let Some(rewritten) = rewrite_intra_doc_link(db, definition, target, title) {
54                 return rewritten;
55             }
56             if let Definition::ModuleDef(def) = definition {
57                 if let Some(target) = rewrite_url_link(db, Either::Left(def), target) {
58                     return (target, title.to_string());
59                 }
60             }
61
62             (target.to_string(), title.to_string())
63         }
64     });
65     let mut out = String::new();
66     cmark_with_options(
67         doc,
68         &mut out,
69         None,
70         CmarkOptions { code_block_backticks: 3, ..Default::default() },
71     )
72     .ok();
73     out
74 }
75
76 /// Remove all links in markdown documentation.
77 pub(crate) fn remove_links(markdown: &str) -> String {
78     let mut drop_link = false;
79
80     let opts = Options::ENABLE_TASKLISTS | Options::ENABLE_FOOTNOTES;
81
82     let mut cb = |_: BrokenLink| {
83         let empty = InlineStr::try_from("").unwrap();
84         Some((CowStr::Inlined(empty), CowStr::Inlined(empty)))
85     };
86     let doc = Parser::new_with_broken_link_callback(markdown, opts, Some(&mut cb));
87     let doc = doc.filter_map(move |evt| match evt {
88         Event::Start(Tag::Link(link_type, target, title)) => {
89             if link_type == LinkType::Inline && target.contains("://") {
90                 Some(Event::Start(Tag::Link(link_type, target, title)))
91             } else {
92                 drop_link = true;
93                 None
94             }
95         }
96         Event::End(_) if drop_link => {
97             drop_link = false;
98             None
99         }
100         _ => Some(evt),
101     });
102
103     let mut out = String::new();
104     cmark_with_options(
105         doc,
106         &mut out,
107         None,
108         CmarkOptions { code_block_backticks: 3, ..Default::default() },
109     )
110     .ok();
111     out
112 }
113
114 /// Retrieve a link to documentation for the given symbol.
115 pub(crate) fn external_docs(
116     db: &RootDatabase,
117     position: &FilePosition,
118 ) -> Option<DocumentationLink> {
119     let sema = &Semantics::new(db);
120     let file = sema.parse(position.file_id).syntax().clone();
121     let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
122         IDENT | INT_NUMBER | T![self] => 3,
123         T!['('] | T![')'] => 2,
124         kind if kind.is_trivia() => 0,
125         _ => 1,
126     })?;
127     let token = sema.descend_into_macros_single(token);
128
129     let node = token.parent()?;
130     let definition = match_ast! {
131         match node {
132             ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? {
133                 NameRefClass::Definition(def) => def,
134                 NameRefClass::FieldShorthand { local_ref: _, field_ref } => {
135                     Definition::Field(field_ref)
136                 }
137             },
138             ast::Name(name) => match NameClass::classify(sema, &name)? {
139                 NameClass::Definition(it) | NameClass::ConstReference(it) => it,
140                 NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref),
141             },
142             _ => return None,
143         }
144     };
145
146     get_doc_link(db, definition)
147 }
148
149 /// Extracts all links from a given markdown text returning the definition text range, link-text
150 /// and the namespace if known.
151 pub(crate) fn extract_definitions_from_docs(
152     docs: &hir::Documentation,
153 ) -> Vec<(TextRange, String, Option<hir::Namespace>)> {
154     Parser::new_with_broken_link_callback(
155         docs.as_str(),
156         Options::ENABLE_TASKLISTS,
157         Some(&mut broken_link_clone_cb),
158     )
159     .into_offset_iter()
160     .filter_map(|(event, range)| match event {
161         Event::Start(Tag::Link(_, target, _)) => {
162             let (link, ns) = parse_intra_doc_link(&target);
163             Some((
164                 TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?),
165                 link.to_string(),
166                 ns,
167             ))
168         }
169         _ => None,
170     })
171     .collect()
172 }
173
174 pub(crate) fn resolve_doc_path_for_def(
175     db: &dyn HirDatabase,
176     def: Definition,
177     link: &str,
178     ns: Option<hir::Namespace>,
179 ) -> Option<Either<ModuleDef, MacroDef>> {
180     match def {
181         Definition::ModuleDef(def) => match def {
182             hir::ModuleDef::Module(it) => it.resolve_doc_path(db, link, ns),
183             hir::ModuleDef::Function(it) => it.resolve_doc_path(db, link, ns),
184             hir::ModuleDef::Adt(it) => it.resolve_doc_path(db, link, ns),
185             hir::ModuleDef::Variant(it) => it.resolve_doc_path(db, link, ns),
186             hir::ModuleDef::Const(it) => it.resolve_doc_path(db, link, ns),
187             hir::ModuleDef::Static(it) => it.resolve_doc_path(db, link, ns),
188             hir::ModuleDef::Trait(it) => it.resolve_doc_path(db, link, ns),
189             hir::ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, link, ns),
190             hir::ModuleDef::BuiltinType(_) => None,
191         },
192         Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
193         Definition::Field(it) => it.resolve_doc_path(db, link, ns),
194         Definition::SelfType(_)
195         | Definition::Local(_)
196         | Definition::GenericParam(_)
197         | Definition::Label(_) => None,
198     }
199 }
200
201 pub(crate) fn doc_attributes(
202     sema: &Semantics<RootDatabase>,
203     node: &SyntaxNode,
204 ) -> Option<(hir::AttrsWithOwner, Definition)> {
205     match_ast! {
206         match node {
207             ast::SourceFile(it)  => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
208             ast::Module(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
209             ast::Fn(it)          => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))),
210             ast::Struct(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))),
211             ast::Union(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))),
212             ast::Enum(it)        => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))),
213             ast::Variant(it)     => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))),
214             ast::Trait(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))),
215             ast::Static(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))),
216             ast::Const(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))),
217             ast::TypeAlias(it)   => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))),
218             ast::Impl(it)        => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))),
219             ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
220             ast::TupleField(it)  => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
221             ast::Macro(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))),
222             // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
223             _ => None
224         }
225     }
226 }
227
228 pub(crate) struct DocCommentToken {
229     doc_token: SyntaxToken,
230     prefix_len: TextSize,
231 }
232
233 pub(crate) fn token_as_doc_comment(doc_token: &SyntaxToken) -> Option<DocCommentToken> {
234     (match_ast! {
235         match doc_token {
236             ast::Comment(comment) => TextSize::try_from(comment.prefix().len()).ok(),
237             ast::String(string) => doc_token.ancestors().find_map(ast::Attr::cast)
238                 .filter(|attr| attr.simple_name().as_deref() == Some("doc")).and_then(|_| string.open_quote_text_range().map(|it| it.len())),
239             _ => None,
240         }
241     }).map(|prefix_len| DocCommentToken { prefix_len, doc_token: doc_token.clone() })
242 }
243
244 impl DocCommentToken {
245     pub(crate) fn get_definition_with_descend_at<T>(
246         self,
247         sema: &Semantics<RootDatabase>,
248         offset: TextSize,
249         // Definition, CommentOwner, range of intra doc link in original file
250         mut cb: impl FnMut(Definition, SyntaxNode, TextRange) -> Option<T>,
251     ) -> Option<T> {
252         let DocCommentToken { prefix_len, doc_token } = self;
253         // offset relative to the comments contents
254         let original_start = doc_token.text_range().start();
255         let relative_comment_offset = offset - original_start - prefix_len;
256
257         sema.descend_into_macros(doc_token).into_iter().find_map(|t| {
258             let (node, descended_prefix_len) = match_ast! {
259                 match t {
260                     ast::Comment(comment) => (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?),
261                     ast::String(string) => (t.ancestors().skip_while(|n| n.kind() != ATTR).nth(1)?, string.open_quote_text_range()?.len()),
262                     _ => return None,
263                 }
264             };
265             let token_start = t.text_range().start();
266             let abs_in_expansion_offset = token_start + relative_comment_offset + descended_prefix_len;
267
268             let (attributes, def) = doc_attributes(sema, &node)?;
269             let (docs, doc_mapping) = attributes.docs_with_rangemap(sema.db)?;
270             let (in_expansion_range, link, ns) =
271                 extract_definitions_from_docs(&docs).into_iter().find_map(|(range, link, ns)| {
272                     let mapped = doc_mapping.map(range)?;
273                     (mapped.value.contains(abs_in_expansion_offset)).then(|| (mapped.value, link, ns))
274                 })?;
275             // get the relative range to the doc/attribute in the expansion
276             let in_expansion_relative_range = in_expansion_range - descended_prefix_len - token_start;
277             // Apply relative range to the original input comment
278             let absolute_range = in_expansion_relative_range + original_start + prefix_len;
279             let def = match resolve_doc_path_for_def(sema.db, def, &link, ns)? {
280                 Either::Left(it) => Definition::ModuleDef(it),
281                 Either::Right(it) => Definition::Macro(it),
282             };
283             cb(def, node, absolute_range)
284         })
285     }
286 }
287
288 fn broken_link_clone_cb<'a, 'b>(link: BrokenLink<'a>) -> Option<(CowStr<'b>, CowStr<'b>)> {
289     // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong
290     // this is fixed in the repo but not on the crates.io release yet
291     Some((
292         /*url*/ link.reference.to_owned().into(),
293         /*title*/ link.reference.to_owned().into(),
294     ))
295 }
296
297 // FIXME:
298 // BUG: For Option::Some
299 // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
300 // Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
301 //
302 // This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
303 // https://github.com/rust-lang/rfcs/pull/2988
304 fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
305     let (target, frag) = match definition {
306         Definition::ModuleDef(def) => {
307             if let Some(assoc_item) = def.as_assoc_item(db) {
308                 let def = match assoc_item.container(db) {
309                     AssocItemContainer::Trait(t) => t.into(),
310                     AssocItemContainer::Impl(i) => i.self_ty(db).as_adt()?.into(),
311                 };
312                 let frag = get_assoc_item_fragment(db, assoc_item)?;
313                 (Either::Left(def), Some(frag))
314             } else {
315                 (Either::Left(def), None)
316             }
317         }
318         Definition::Field(field) => {
319             let def = match field.parent_def(db) {
320                 hir::VariantDef::Struct(it) => it.into(),
321                 hir::VariantDef::Union(it) => it.into(),
322                 hir::VariantDef::Variant(it) => it.into(),
323             };
324             (Either::Left(def), Some(format!("structfield.{}", field.name(db))))
325         }
326         Definition::Macro(makro) => (Either::Right(makro), None),
327         // FIXME impls
328         Definition::SelfType(_) => return None,
329         Definition::Local(_) | Definition::GenericParam(_) | Definition::Label(_) => return None,
330     };
331
332     let krate = crate_of_def(db, target)?;
333     let mut url = get_doc_base_url(db, &krate)?;
334
335     if let Some(path) = mod_path_of_def(db, target) {
336         url = url.join(&path).ok()?;
337     }
338
339     url = url.join(&get_symbol_filename(db, target)?).ok()?;
340     url.set_fragment(frag.as_deref());
341
342     Some(url.into())
343 }
344
345 fn rewrite_intra_doc_link(
346     db: &RootDatabase,
347     def: Definition,
348     target: &str,
349     title: &str,
350 ) -> Option<(String, String)> {
351     let (link, ns) = parse_intra_doc_link(target);
352
353     let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
354     let krate = crate_of_def(db, resolved)?;
355     let mut url = get_doc_base_url(db, &krate)?;
356
357     if let Some(path) = mod_path_of_def(db, resolved) {
358         url = url.join(&path).ok()?;
359     }
360
361     let (resolved, frag) =
362         if let Some(assoc_item) = resolved.left().and_then(|it| it.as_assoc_item(db)) {
363             let resolved = match assoc_item.container(db) {
364                 AssocItemContainer::Trait(t) => t.into(),
365                 AssocItemContainer::Impl(i) => i.self_ty(db).as_adt()?.into(),
366             };
367             let frag = get_assoc_item_fragment(db, assoc_item)?;
368             (Either::Left(resolved), Some(frag))
369         } else {
370             (resolved, None)
371         };
372     url = url.join(&get_symbol_filename(db, resolved)?).ok()?;
373     url.set_fragment(frag.as_deref());
374
375     Some((url.into(), strip_prefixes_suffixes(title).to_string()))
376 }
377
378 /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
379 fn rewrite_url_link(
380     db: &RootDatabase,
381     def: Either<ModuleDef, MacroDef>,
382     target: &str,
383 ) -> Option<String> {
384     if !(target.contains('#') || target.contains(".html")) {
385         return None;
386     }
387
388     let krate = crate_of_def(db, def)?;
389     let mut url = get_doc_base_url(db, &krate)?;
390
391     if let Some(path) = mod_path_of_def(db, def) {
392         url = url.join(&path).ok()?;
393     }
394
395     url = url.join(&get_symbol_filename(db, def)?).ok()?;
396     url.join(target).ok().map(Into::into)
397 }
398
399 fn crate_of_def(db: &RootDatabase, def: Either<ModuleDef, MacroDef>) -> Option<Crate> {
400     let krate = match def {
401         // Definition::module gives back the parent module, we don't want that as it fails for root modules
402         Either::Left(ModuleDef::Module(module)) => module.krate(),
403         Either::Left(def) => def.module(db)?.krate(),
404         Either::Right(def) => def.module(db)?.krate(),
405     };
406     Some(krate)
407 }
408
409 fn mod_path_of_def(db: &RootDatabase, def: Either<ModuleDef, MacroDef>) -> Option<String> {
410     match def {
411         Either::Left(def) => def.canonical_module_path(db).map(|it| {
412             let mut path = String::new();
413             it.flat_map(|it| it.name(db)).for_each(|name| format_to!(path, "{}/", name));
414             path
415         }),
416         Either::Right(def) => {
417             def.module(db).map(|it| it.path_to_root(db).into_iter().rev()).map(|it| {
418                 let mut path = String::new();
419                 it.flat_map(|it| it.name(db)).for_each(|name| format_to!(path, "{}/", name));
420                 path
421             })
422         }
423     }
424 }
425
426 /// Rewrites a markdown document, applying 'callback' to each link.
427 fn map_links<'e>(
428     events: impl Iterator<Item = Event<'e>>,
429     callback: impl Fn(&str, &str) -> (String, String),
430 ) -> impl Iterator<Item = Event<'e>> {
431     let mut in_link = false;
432     let mut link_target: Option<CowStr> = None;
433
434     events.map(move |evt| match evt {
435         Event::Start(Tag::Link(_, ref target, _)) => {
436             in_link = true;
437             link_target = Some(target.clone());
438             evt
439         }
440         Event::End(Tag::Link(link_type, target, _)) => {
441             in_link = false;
442             Event::End(Tag::Link(
443                 link_type,
444                 link_target.take().unwrap_or(target),
445                 CowStr::Borrowed(""),
446             ))
447         }
448         Event::Text(s) if in_link => {
449             let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
450             link_target = Some(CowStr::Boxed(link_target_s.into()));
451             Event::Text(CowStr::Boxed(link_name.into()))
452         }
453         Event::Code(s) if in_link => {
454             let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
455             link_target = Some(CowStr::Boxed(link_target_s.into()));
456             Event::Code(CowStr::Boxed(link_name.into()))
457         }
458         _ => evt,
459     })
460 }
461
462 /// Get the root URL for the documentation of a crate.
463 ///
464 /// ```ignore
465 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
466 /// ^^^^^^^^^^^^^^^^^^^^^^^^^^
467 /// ```
468 fn get_doc_base_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
469     let display_name = krate.display_name(db)?;
470     krate
471         .get_html_root_url(db)
472         .or_else(|| {
473             // Fallback to docs.rs. This uses `display_name` and can never be
474             // correct, but that's what fallbacks are about.
475             //
476             // FIXME: clicking on the link should just open the file in the editor,
477             // instead of falling back to external urls.
478             Some(format!("https://docs.rs/{krate}/*/", krate = display_name))
479         })
480         .and_then(|s| Url::parse(&s).ok()?.join(&format!("{}/", display_name)).ok())
481 }
482
483 /// Get the filename and extension generated for a symbol by rustdoc.
484 ///
485 /// ```ignore
486 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
487 ///                                    ^^^^^^^^^^^^^^^^^^^
488 /// ```
489 fn get_symbol_filename(
490     db: &dyn HirDatabase,
491     definition: Either<ModuleDef, MacroDef>,
492 ) -> Option<String> {
493     let res = match definition {
494         Either::Left(definition) => match definition {
495             ModuleDef::Adt(adt) => match adt {
496                 Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
497                 Adt::Enum(e) => format!("enum.{}.html", e.name(db)),
498                 Adt::Union(u) => format!("union.{}.html", u.name(db)),
499             },
500             ModuleDef::Module(m) => match m.name(db) {
501                 Some(name) => format!("{}/index.html", name),
502                 None => String::from("index.html"),
503             },
504             ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
505             ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
506             ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.name()),
507             ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
508             ModuleDef::Variant(ev) => {
509                 format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
510             }
511             ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?),
512             ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
513         },
514         Either::Right(mac) => format!("macro.{}.html", mac.name(db)?),
515     };
516     Some(res)
517 }
518
519 /// Get the fragment required to link to a specific field, method, associated type, or associated constant.
520 ///
521 /// ```ignore
522 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
523 ///                                                       ^^^^^^^^^^^^^^
524 /// ```
525 fn get_assoc_item_fragment(db: &dyn HirDatabase, assoc_item: hir::AssocItem) -> Option<String> {
526     Some(match assoc_item {
527         AssocItem::Function(function) => {
528             let is_trait_method =
529                 function.as_assoc_item(db).and_then(|assoc| assoc.containing_trait(db)).is_some();
530             // This distinction may get more complicated when specialization is available.
531             // Rustdoc makes this decision based on whether a method 'has defaultness'.
532             // Currently this is only the case for provided trait methods.
533             if is_trait_method && !function.has_body(db) {
534                 format!("tymethod.{}", function.name(db))
535             } else {
536                 format!("method.{}", function.name(db))
537             }
538         }
539         AssocItem::Const(constant) => format!("associatedconstant.{}", constant.name(db)?),
540         AssocItem::TypeAlias(ty) => format!("associatedtype.{}", ty.name(db)),
541     })
542 }
543
544 #[cfg(test)]
545 mod tests {
546     use expect_test::{expect, Expect};
547     use ide_db::base_db::FileRange;
548     use itertools::Itertools;
549
550     use crate::{display::TryToNav, fixture};
551
552     use super::*;
553
554     #[test]
555     fn external_docs_doc_url_crate() {
556         check_external_docs(
557             r#"
558 //- /main.rs crate:main deps:test
559 use test$0::Foo;
560 //- /lib.rs crate:test
561 pub struct Foo;
562 "#,
563             expect![[r#"https://docs.rs/test/*/test/index.html"#]],
564         );
565     }
566
567     #[test]
568     fn external_docs_doc_url_struct() {
569         check_external_docs(
570             r#"
571 pub struct Fo$0o;
572 "#,
573             expect![[r#"https://docs.rs/test/*/test/struct.Foo.html"#]],
574         );
575     }
576
577     #[test]
578     fn external_docs_doc_url_struct_field() {
579         check_external_docs(
580             r#"
581 pub struct Foo {
582     field$0: ()
583 }
584 "#,
585             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#structfield.field"##]],
586         );
587     }
588
589     #[test]
590     fn external_docs_doc_url_fn() {
591         check_external_docs(
592             r#"
593 pub fn fo$0o() {}
594 "#,
595             expect![[r##"https://docs.rs/test/*/test/fn.foo.html"##]],
596         );
597     }
598
599     #[test]
600     fn external_docs_doc_url_impl_assoc() {
601         check_external_docs(
602             r#"
603 pub struct Foo;
604 impl Foo {
605     pub fn method$0() {}
606 }
607 "#,
608             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#method.method"##]],
609         );
610         check_external_docs(
611             r#"
612 pub struct Foo;
613 impl Foo {
614     const CONST$0: () = ();
615 }
616 "#,
617             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#associatedconstant.CONST"##]],
618         );
619     }
620
621     #[test]
622     fn external_docs_doc_url_impl_trait_assoc() {
623         check_external_docs(
624             r#"
625 pub struct Foo;
626 pub trait Trait {
627     fn method() {}
628 }
629 impl Trait for Foo {
630     pub fn method$0() {}
631 }
632 "#,
633             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#method.method"##]],
634         );
635         check_external_docs(
636             r#"
637 pub struct Foo;
638 pub trait Trait {
639     const CONST: () = ();
640 }
641 impl Trait for Foo {
642     const CONST$0: () = ();
643 }
644 "#,
645             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#associatedconstant.CONST"##]],
646         );
647         check_external_docs(
648             r#"
649 pub struct Foo;
650 pub trait Trait {
651     type Type;
652 }
653 impl Trait for Foo {
654     type Type$0 = ();
655 }
656 "#,
657             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#associatedtype.Type"##]],
658         );
659     }
660
661     #[test]
662     fn external_docs_doc_url_trait_assoc() {
663         check_external_docs(
664             r#"
665 pub trait Foo {
666     fn method$0();
667 }
668 "#,
669             expect![[r##"https://docs.rs/test/*/test/trait.Foo.html#tymethod.method"##]],
670         );
671         check_external_docs(
672             r#"
673 pub trait Foo {
674     const CONST$0: ();
675 }
676 "#,
677             expect![[r##"https://docs.rs/test/*/test/trait.Foo.html#associatedconstant.CONST"##]],
678         );
679         check_external_docs(
680             r#"
681 pub trait Foo {
682     type Type$0;
683 }
684 "#,
685             expect![[r##"https://docs.rs/test/*/test/trait.Foo.html#associatedtype.Type"##]],
686         );
687     }
688
689     #[test]
690     fn external_docs_trait() {
691         check_external_docs(
692             r#"
693 trait Trait$0 {}
694 "#,
695             expect![[r#"https://docs.rs/test/*/test/trait.Trait.html"#]],
696         )
697     }
698
699     #[test]
700     fn external_docs_module() {
701         check_external_docs(
702             r#"
703 pub mod foo {
704     pub mod ba$0r {}
705 }
706 "#,
707             expect![[r#"https://docs.rs/test/*/test/foo/bar/index.html"#]],
708         )
709     }
710
711     #[test]
712     fn external_docs_reexport_order() {
713         check_external_docs(
714             r#"
715 pub mod wrapper {
716     pub use module::Item;
717
718     pub mod module {
719         pub struct Item;
720     }
721 }
722
723 fn foo() {
724     let bar: wrapper::It$0em;
725 }
726         "#,
727             expect![[r#"https://docs.rs/test/*/test/wrapper/module/struct.Item.html"#]],
728         )
729     }
730
731     #[test]
732     fn test_trait_items() {
733         check_doc_links(
734             r#"
735 /// [`Trait`]
736 /// [`Trait::Type`]
737 /// [`Trait::CONST`]
738 /// [`Trait::func`]
739 trait Trait$0 {
740    // ^^^^^ Trait
741     type Type;
742       // ^^^^ Trait::Type
743     const CONST: usize;
744        // ^^^^^ Trait::CONST
745     fn func();
746     // ^^^^ Trait::func
747 }
748         "#,
749         )
750     }
751
752     #[test]
753     fn rewrite_html_root_url() {
754         check_rewrite(
755             r#"
756 #![doc(arbitrary_attribute = "test", html_root_url = "https:/example.com", arbitrary_attribute2)]
757
758 pub mod foo {
759     pub struct Foo;
760 }
761 /// [Foo](foo::Foo)
762 pub struct B$0ar
763 "#,
764             expect![[r#"[Foo](https://example.com/test/foo/struct.Foo.html)"#]],
765         );
766     }
767
768     #[test]
769     fn rewrite_on_field() {
770         // FIXME: Should be
771         //  [Foo](https://docs.rs/test/*/test/struct.Foo.html)
772         check_rewrite(
773             r#"
774 pub struct Foo {
775     /// [Foo](struct.Foo.html)
776     fie$0ld: ()
777 }
778 "#,
779             expect![[r#"[Foo](struct.Foo.html)"#]],
780         );
781     }
782
783     #[test]
784     fn rewrite_struct() {
785         check_rewrite(
786             r#"
787 /// [Foo]
788 pub struct $0Foo;
789 "#,
790             expect![[r#"[Foo](https://docs.rs/test/*/test/struct.Foo.html)"#]],
791         );
792         check_rewrite(
793             r#"
794 /// [`Foo`]
795 pub struct $0Foo;
796 "#,
797             expect![[r#"[`Foo`](https://docs.rs/test/*/test/struct.Foo.html)"#]],
798         );
799         check_rewrite(
800             r#"
801 /// [Foo](struct.Foo.html)
802 pub struct $0Foo;
803 "#,
804             expect![[r#"[Foo](https://docs.rs/test/*/test/struct.Foo.html)"#]],
805         );
806         check_rewrite(
807             r#"
808 /// [struct Foo](struct.Foo.html)
809 pub struct $0Foo;
810 "#,
811             expect![[r#"[struct Foo](https://docs.rs/test/*/test/struct.Foo.html)"#]],
812         );
813         check_rewrite(
814             r#"
815 /// [my Foo][foo]
816 ///
817 /// [foo]: Foo
818 pub struct $0Foo;
819 "#,
820             expect![[r#"[my Foo](https://docs.rs/test/*/test/struct.Foo.html)"#]],
821         );
822     }
823
824     fn check_external_docs(ra_fixture: &str, expect: Expect) {
825         let (analysis, position) = fixture::position(ra_fixture);
826         let url = analysis.external_docs(position).unwrap().expect("could not find url for symbol");
827
828         expect.assert_eq(&url)
829     }
830
831     fn check_rewrite(ra_fixture: &str, expect: Expect) {
832         let (analysis, position) = fixture::position(ra_fixture);
833         let sema = &Semantics::new(&*analysis.db);
834         let (cursor_def, docs) = def_under_cursor(sema, &position);
835         let res = rewrite_links(sema.db, docs.as_str(), cursor_def);
836         expect.assert_eq(&res)
837     }
838
839     fn check_doc_links(ra_fixture: &str) {
840         let key_fn = |&(FileRange { file_id, range }, _): &_| (file_id, range.start());
841
842         let (analysis, position, mut expected) = fixture::annotations(ra_fixture);
843         expected.sort_by_key(key_fn);
844         let sema = &Semantics::new(&*analysis.db);
845         let (cursor_def, docs) = def_under_cursor(sema, &position);
846         let defs = extract_definitions_from_docs(&docs);
847         let actual: Vec<_> = defs
848             .into_iter()
849             .map(|(_, link, ns)| {
850                 let def = resolve_doc_path_for_def(sema.db, cursor_def, &link, ns)
851                     .unwrap_or_else(|| panic!("Failed to resolve {}", link));
852                 let nav_target = def.try_to_nav(sema.db).unwrap();
853                 let range = FileRange {
854                     file_id: nav_target.file_id,
855                     range: nav_target.focus_or_full_range(),
856                 };
857                 (range, link)
858             })
859             .sorted_by_key(key_fn)
860             .collect();
861         assert_eq!(expected, actual);
862     }
863
864     fn def_under_cursor(
865         sema: &Semantics<RootDatabase>,
866         position: &FilePosition,
867     ) -> (Definition, hir::Documentation) {
868         let (docs, def) = sema
869             .parse(position.file_id)
870             .syntax()
871             .token_at_offset(position.offset)
872             .left_biased()
873             .unwrap()
874             .ancestors()
875             .find_map(|it| node_to_def(sema, &it))
876             .expect("no def found")
877             .unwrap();
878         let docs = docs.expect("no docs found for cursor def");
879         (def, docs)
880     }
881
882     fn node_to_def(
883         sema: &Semantics<RootDatabase>,
884         node: &SyntaxNode,
885     ) -> Option<Option<(Option<hir::Documentation>, Definition)>> {
886         Some(match_ast! {
887             match node {
888                 ast::SourceFile(it)  => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
889                 ast::Module(it)      => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
890                 ast::Fn(it)          => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))),
891                 ast::Struct(it)      => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))),
892                 ast::Union(it)       => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))),
893                 ast::Enum(it)        => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))),
894                 ast::Variant(it)     => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))),
895                 ast::Trait(it)       => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))),
896                 ast::Static(it)      => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))),
897                 ast::Const(it)       => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))),
898                 ast::TypeAlias(it)   => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))),
899                 ast::Impl(it)        => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::SelfType(def))),
900                 ast::RecordField(it) => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Field(def))),
901                 ast::TupleField(it)  => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Field(def))),
902                 ast::Macro(it)       => sema.to_def(&it).map(|def| (def.docs(sema.db), Definition::Macro(def))),
903                 // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
904                 _ => return None,
905             }
906         })
907     }
908 }