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