• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Extracts, resolves and rewrites links and intra-doc links in markdown documentation.
2 
3 #[cfg(test)]
4 mod tests;
5 
6 mod intra_doc_links;
7 
8 use std::ffi::OsStr;
9 
10 use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
11 use pulldown_cmark_to_cmark::{cmark_resume_with_options, Options as CMarkOptions};
12 use stdx::format_to;
13 use url::Url;
14 
15 use hir::{db::HirDatabase, Adt, AsAssocItem, AssocItem, AssocItemContainer, HasAttrs};
16 use ide_db::{
17     base_db::{CrateOrigin, LangCrateOrigin, ReleaseChannel, SourceDatabase},
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 /// Web and local links to an item's documentation.
35 #[derive(Default, Debug, Clone, PartialEq, Eq)]
36 pub struct DocumentationLinks {
37     /// The URL to the documentation on docs.rs.
38     /// May not lead anywhere.
39     pub web_url: Option<String>,
40     /// The URL to the documentation in the local file system.
41     /// May not lead anywhere.
42     pub local_url: Option<String>,
43 }
44 
45 const MARKDOWN_OPTIONS: Options =
46     Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS);
47 
48 /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
rewrite_links(db: &RootDatabase, markdown: &str, definition: Definition) -> String49 pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: Definition) -> String {
50     let mut cb = broken_link_clone_cb;
51     let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb));
52 
53     let doc = map_links(doc, |target, title| {
54         // This check is imperfect, there's some overlap between valid intra-doc links
55         // and valid URLs so we choose to be too eager to try to resolve what might be
56         // a URL.
57         if target.contains("://") {
58             (Some(LinkType::Inline), target.to_string(), title.to_string())
59         } else {
60             // Two possibilities:
61             // * path-based links: `../../module/struct.MyStruct.html`
62             // * module-based links (AKA intra-doc links): `super::super::module::MyStruct`
63             if let Some((target, title)) = rewrite_intra_doc_link(db, definition, target, title) {
64                 return (None, target, title);
65             }
66             if let Some(target) = rewrite_url_link(db, definition, target) {
67                 return (Some(LinkType::Inline), target, title.to_string());
68             }
69 
70             (None, target.to_string(), title.to_string())
71         }
72     });
73     let mut out = String::new();
74     cmark_resume_with_options(
75         doc,
76         &mut out,
77         None,
78         CMarkOptions { code_block_token_count: 3, ..Default::default() },
79     )
80     .ok();
81     out
82 }
83 
84 /// Remove all links in markdown documentation.
remove_links(markdown: &str) -> String85 pub(crate) fn remove_links(markdown: &str) -> String {
86     let mut drop_link = false;
87 
88     let mut cb = |_: BrokenLink<'_>| {
89         let empty = InlineStr::try_from("").unwrap();
90         Some((CowStr::Inlined(empty), CowStr::Inlined(empty)))
91     };
92     let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb));
93     let doc = doc.filter_map(move |evt| match evt {
94         Event::Start(Tag::Link(link_type, target, title)) => {
95             if link_type == LinkType::Inline && target.contains("://") {
96                 Some(Event::Start(Tag::Link(link_type, target, title)))
97             } else {
98                 drop_link = true;
99                 None
100             }
101         }
102         Event::End(_) if drop_link => {
103             drop_link = false;
104             None
105         }
106         _ => Some(evt),
107     });
108 
109     let mut out = String::new();
110     cmark_resume_with_options(
111         doc,
112         &mut out,
113         None,
114         CMarkOptions { code_block_token_count: 3, ..Default::default() },
115     )
116     .ok();
117     out
118 }
119 
120 // Feature: Open Docs
121 //
122 // Retrieve a links to documentation for the given symbol.
123 //
124 // The simplest way to use this feature is via the context menu. Right-click on
125 // the selected item. The context menu opens. Select **Open Docs**.
126 //
127 // |===
128 // | Editor  | Action Name
129 //
130 // | VS Code | **rust-analyzer: Open Docs**
131 // |===
external_docs( db: &RootDatabase, position: &FilePosition, target_dir: Option<&OsStr>, sysroot: Option<&OsStr>, ) -> Option<DocumentationLinks>132 pub(crate) fn external_docs(
133     db: &RootDatabase,
134     position: &FilePosition,
135     target_dir: Option<&OsStr>,
136     sysroot: Option<&OsStr>,
137 ) -> Option<DocumentationLinks> {
138     let sema = &Semantics::new(db);
139     let file = sema.parse(position.file_id).syntax().clone();
140     let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
141         IDENT | INT_NUMBER | T![self] => 3,
142         T!['('] | T![')'] => 2,
143         kind if kind.is_trivia() => 0,
144         _ => 1,
145     })?;
146     let token = sema.descend_into_macros_single(token);
147 
148     let node = token.parent()?;
149     let definition = match_ast! {
150         match node {
151             ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? {
152                 NameRefClass::Definition(def) => def,
153                 NameRefClass::FieldShorthand { local_ref: _, field_ref } => {
154                     Definition::Field(field_ref)
155                 }
156             },
157             ast::Name(name) => match NameClass::classify(sema, &name)? {
158                 NameClass::Definition(it) | NameClass::ConstReference(it) => it,
159                 NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref),
160             },
161             _ => return None
162         }
163     };
164 
165     Some(get_doc_links(db, definition, target_dir, sysroot))
166 }
167 
168 /// Extracts all links from a given markdown text returning the definition text range, link-text
169 /// and the namespace if known.
extract_definitions_from_docs( docs: &hir::Documentation, ) -> Vec<(TextRange, String, Option<hir::Namespace>)>170 pub(crate) fn extract_definitions_from_docs(
171     docs: &hir::Documentation,
172 ) -> Vec<(TextRange, String, Option<hir::Namespace>)> {
173     Parser::new_with_broken_link_callback(
174         docs.as_str(),
175         MARKDOWN_OPTIONS,
176         Some(&mut broken_link_clone_cb),
177     )
178     .into_offset_iter()
179     .filter_map(|(event, range)| match event {
180         Event::Start(Tag::Link(_, target, _)) => {
181             let (link, ns) = parse_intra_doc_link(&target);
182             Some((
183                 TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?),
184                 link.to_string(),
185                 ns,
186             ))
187         }
188         _ => None,
189     })
190     .collect()
191 }
192 
resolve_doc_path_for_def( db: &dyn HirDatabase, def: Definition, link: &str, ns: Option<hir::Namespace>, ) -> Option<Definition>193 pub(crate) fn resolve_doc_path_for_def(
194     db: &dyn HirDatabase,
195     def: Definition,
196     link: &str,
197     ns: Option<hir::Namespace>,
198 ) -> Option<Definition> {
199     match def {
200         Definition::Module(it) => it.resolve_doc_path(db, link, ns),
201         Definition::Function(it) => it.resolve_doc_path(db, link, ns),
202         Definition::Adt(it) => it.resolve_doc_path(db, link, ns),
203         Definition::Variant(it) => it.resolve_doc_path(db, link, ns),
204         Definition::Const(it) => it.resolve_doc_path(db, link, ns),
205         Definition::Static(it) => it.resolve_doc_path(db, link, ns),
206         Definition::Trait(it) => it.resolve_doc_path(db, link, ns),
207         Definition::TraitAlias(it) => it.resolve_doc_path(db, link, ns),
208         Definition::TypeAlias(it) => it.resolve_doc_path(db, link, ns),
209         Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
210         Definition::Field(it) => it.resolve_doc_path(db, link, ns),
211         Definition::SelfType(it) => it.resolve_doc_path(db, link, ns),
212         Definition::BuiltinAttr(_)
213         | Definition::ToolModule(_)
214         | Definition::BuiltinType(_)
215         | Definition::Local(_)
216         | Definition::GenericParam(_)
217         | Definition::Label(_)
218         | Definition::DeriveHelper(_) => None,
219     }
220     .map(Definition::from)
221 }
222 
doc_attributes( sema: &Semantics<'_, RootDatabase>, node: &SyntaxNode, ) -> Option<(hir::AttrsWithOwner, Definition)>223 pub(crate) fn doc_attributes(
224     sema: &Semantics<'_, RootDatabase>,
225     node: &SyntaxNode,
226 ) -> Option<(hir::AttrsWithOwner, Definition)> {
227     match_ast! {
228         match node {
229             ast::SourceFile(it)  => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Module(def))),
230             ast::Module(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Module(def))),
231             ast::Fn(it)          => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Function(def))),
232             ast::Struct(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Struct(def)))),
233             ast::Union(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Union(def)))),
234             ast::Enum(it)        => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Enum(def)))),
235             ast::Variant(it)     => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Variant(def))),
236             ast::Trait(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Trait(def))),
237             ast::Static(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Static(def))),
238             ast::Const(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Const(def))),
239             ast::TypeAlias(it)   => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::TypeAlias(def))),
240             ast::Impl(it)        => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))),
241             ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
242             ast::TupleField(it)  => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
243             ast::Macro(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))),
244             // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
245             _ => None
246         }
247     }
248 }
249 
250 pub(crate) struct DocCommentToken {
251     doc_token: SyntaxToken,
252     prefix_len: TextSize,
253 }
254 
token_as_doc_comment(doc_token: &SyntaxToken) -> Option<DocCommentToken>255 pub(crate) fn token_as_doc_comment(doc_token: &SyntaxToken) -> Option<DocCommentToken> {
256     (match_ast! {
257         match doc_token {
258             ast::Comment(comment) => TextSize::try_from(comment.prefix().len()).ok(),
259             ast::String(string) => {
260                 doc_token.parent_ancestors().find_map(ast::Attr::cast).filter(|attr| attr.simple_name().as_deref() == Some("doc"))?;
261                 if doc_token.parent_ancestors().find_map(ast::MacroCall::cast).filter(|mac| mac.path().and_then(|p| p.segment()?.name_ref()).as_ref().map(|n| n.text()).as_deref() == Some("include_str")).is_some() {
262                     return None;
263                 }
264                 string.open_quote_text_range().map(|it| it.len())
265             },
266             _ => None,
267         }
268     }).map(|prefix_len| DocCommentToken { prefix_len, doc_token: doc_token.clone() })
269 }
270 
271 impl DocCommentToken {
get_definition_with_descend_at<T>( self, sema: &Semantics<'_, RootDatabase>, offset: TextSize, mut cb: impl FnMut(Definition, SyntaxNode, TextRange) -> Option<T>, ) -> Option<T>272     pub(crate) fn get_definition_with_descend_at<T>(
273         self,
274         sema: &Semantics<'_, RootDatabase>,
275         offset: TextSize,
276         // Definition, CommentOwner, range of intra doc link in original file
277         mut cb: impl FnMut(Definition, SyntaxNode, TextRange) -> Option<T>,
278     ) -> Option<T> {
279         let DocCommentToken { prefix_len, doc_token } = self;
280         // offset relative to the comments contents
281         let original_start = doc_token.text_range().start();
282         let relative_comment_offset = offset - original_start - prefix_len;
283 
284         sema.descend_into_macros(doc_token).into_iter().find_map(|t| {
285             let (node, descended_prefix_len) = match_ast! {
286                 match t {
287                     ast::Comment(comment) => (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?),
288                     ast::String(string) => (t.parent_ancestors().skip_while(|n| n.kind() != ATTR).nth(1)?, string.open_quote_text_range()?.len()),
289                     _ => return None,
290                 }
291             };
292             let token_start = t.text_range().start();
293             let abs_in_expansion_offset = token_start + relative_comment_offset + descended_prefix_len;
294 
295             let (attributes, def) = doc_attributes(sema, &node)?;
296             let (docs, doc_mapping) = attributes.docs_with_rangemap(sema.db)?;
297             let (in_expansion_range, link, ns) =
298                 extract_definitions_from_docs(&docs).into_iter().find_map(|(range, link, ns)| {
299                     let mapped = doc_mapping.map(range)?;
300                     (mapped.value.contains(abs_in_expansion_offset)).then_some((mapped.value, link, ns))
301                 })?;
302             // get the relative range to the doc/attribute in the expansion
303             let in_expansion_relative_range = in_expansion_range - descended_prefix_len - token_start;
304             // Apply relative range to the original input comment
305             let absolute_range = in_expansion_relative_range + original_start + prefix_len;
306             let def = resolve_doc_path_for_def(sema.db, def, &link, ns)?;
307             cb(def, node, absolute_range)
308         })
309     }
310 }
311 
broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)>312 fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)> {
313     Some((/*url*/ link.reference.clone(), /*title*/ link.reference))
314 }
315 
316 // FIXME:
317 // BUG: For Option::Some
318 // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
319 // Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
320 //
321 // This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
322 // https://github.com/rust-lang/rfcs/pull/2988
get_doc_links( db: &RootDatabase, def: Definition, target_dir: Option<&OsStr>, sysroot: Option<&OsStr>, ) -> DocumentationLinks323 fn get_doc_links(
324     db: &RootDatabase,
325     def: Definition,
326     target_dir: Option<&OsStr>,
327     sysroot: Option<&OsStr>,
328 ) -> DocumentationLinks {
329     let join_url = |base_url: Option<Url>, path: &str| -> Option<Url> {
330         base_url.and_then(|url| url.join(path).ok())
331     };
332 
333     let Some((target, file, frag)) = filename_and_frag_for_def(db, def) else { return Default::default(); };
334 
335     let (mut web_url, mut local_url) = get_doc_base_urls(db, target, target_dir, sysroot);
336 
337     if let Some(path) = mod_path_of_def(db, target) {
338         web_url = join_url(web_url, &path);
339         local_url = join_url(local_url, &path);
340     }
341 
342     web_url = join_url(web_url, &file);
343     local_url = join_url(local_url, &file);
344 
345     web_url.as_mut().map(|url| url.set_fragment(frag.as_deref()));
346     local_url.as_mut().map(|url| url.set_fragment(frag.as_deref()));
347 
348     DocumentationLinks {
349         web_url: web_url.map(|it| it.into()),
350         local_url: local_url.map(|it| it.into()),
351     }
352 }
353 
rewrite_intra_doc_link( db: &RootDatabase, def: Definition, target: &str, title: &str, ) -> Option<(String, String)>354 fn rewrite_intra_doc_link(
355     db: &RootDatabase,
356     def: Definition,
357     target: &str,
358     title: &str,
359 ) -> Option<(String, String)> {
360     let (link, ns) = parse_intra_doc_link(target);
361 
362     let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
363     let mut url = get_doc_base_urls(db, resolved, None, None).0?;
364 
365     let (_, file, frag) = filename_and_frag_for_def(db, resolved)?;
366     if let Some(path) = mod_path_of_def(db, resolved) {
367         url = url.join(&path).ok()?;
368     }
369 
370     url = url.join(&file).ok()?;
371     url.set_fragment(frag.as_deref());
372 
373     Some((url.into(), strip_prefixes_suffixes(title).to_string()))
374 }
375 
376 /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<String>377 fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<String> {
378     if !(target.contains('#') || target.contains(".html")) {
379         return None;
380     }
381 
382     let mut url = get_doc_base_urls(db, def, None, None).0?;
383     let (def, file, frag) = filename_and_frag_for_def(db, def)?;
384 
385     if let Some(path) = mod_path_of_def(db, def) {
386         url = url.join(&path).ok()?;
387     }
388 
389     url = url.join(&file).ok()?;
390     url.set_fragment(frag.as_deref());
391     url.join(target).ok().map(Into::into)
392 }
393 
mod_path_of_def(db: &RootDatabase, def: Definition) -> Option<String>394 fn mod_path_of_def(db: &RootDatabase, def: Definition) -> Option<String> {
395     def.canonical_module_path(db).map(|it| {
396         let mut path = String::new();
397         it.flat_map(|it| it.name(db)).for_each(|name| format_to!(path, "{}/", name.display(db)));
398         path
399     })
400 }
401 
402 /// Rewrites a markdown document, applying 'callback' to each link.
map_links<'e>( events: impl Iterator<Item = Event<'e>>, callback: impl Fn(&str, &str) -> (Option<LinkType>, String, String), ) -> impl Iterator<Item = Event<'e>>403 fn map_links<'e>(
404     events: impl Iterator<Item = Event<'e>>,
405     callback: impl Fn(&str, &str) -> (Option<LinkType>, String, String),
406 ) -> impl Iterator<Item = Event<'e>> {
407     let mut in_link = false;
408     // holds the origin link target on start event and the rewritten one on end event
409     let mut end_link_target: Option<CowStr<'_>> = None;
410     // normally link's type is determined by the type of link tag in the end event,
411     // however in some cases we want to change the link type, for example,
412     // `Shortcut` type parsed from Start/End tags doesn't make sense for url links
413     let mut end_link_type: Option<LinkType> = None;
414 
415     events.map(move |evt| match evt {
416         Event::Start(Tag::Link(link_type, ref target, _)) => {
417             in_link = true;
418             end_link_target = Some(target.clone());
419             end_link_type = Some(link_type);
420             evt
421         }
422         Event::End(Tag::Link(link_type, target, _)) => {
423             in_link = false;
424             Event::End(Tag::Link(
425                 end_link_type.unwrap_or(link_type),
426                 end_link_target.take().unwrap_or(target),
427                 CowStr::Borrowed(""),
428             ))
429         }
430         Event::Text(s) if in_link => {
431             let (link_type, link_target_s, link_name) =
432                 callback(&end_link_target.take().unwrap(), &s);
433             end_link_target = Some(CowStr::Boxed(link_target_s.into()));
434             if !matches!(end_link_type, Some(LinkType::Autolink)) {
435                 end_link_type = link_type;
436             }
437             Event::Text(CowStr::Boxed(link_name.into()))
438         }
439         Event::Code(s) if in_link => {
440             let (link_type, link_target_s, link_name) =
441                 callback(&end_link_target.take().unwrap(), &s);
442             end_link_target = Some(CowStr::Boxed(link_target_s.into()));
443             if !matches!(end_link_type, Some(LinkType::Autolink)) {
444                 end_link_type = link_type;
445             }
446             Event::Code(CowStr::Boxed(link_name.into()))
447         }
448         _ => evt,
449     })
450 }
451 
452 /// Get the root URL for the documentation of a definition.
453 ///
454 /// ```ignore
455 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
456 /// ^^^^^^^^^^^^^^^^^^^^^^^^^^
457 /// file:///project/root/target/doc/std/iter/trait.Iterator.html#tymethod.next
458 /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
459 /// ```
get_doc_base_urls( db: &RootDatabase, def: Definition, target_dir: Option<&OsStr>, sysroot: Option<&OsStr>, ) -> (Option<Url>, Option<Url>)460 fn get_doc_base_urls(
461     db: &RootDatabase,
462     def: Definition,
463     target_dir: Option<&OsStr>,
464     sysroot: Option<&OsStr>,
465 ) -> (Option<Url>, Option<Url>) {
466     let local_doc = target_dir
467         .and_then(|path| path.to_str())
468         .and_then(|path| Url::parse(&format!("file:///{path}/")).ok())
469         .and_then(|it| it.join("doc/").ok());
470     let system_doc = sysroot
471         .and_then(|it| it.to_str())
472         .map(|sysroot| format!("file:///{sysroot}/share/doc/rust/html/"))
473         .and_then(|it| Url::parse(&it).ok());
474 
475     // special case base url of `BuiltinType` to core
476     // https://github.com/rust-lang/rust-analyzer/issues/12250
477     if let Definition::BuiltinType(..) = def {
478         let web_link = Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
479         let system_link = system_doc.and_then(|it| it.join("core/").ok());
480         return (web_link, system_link);
481     };
482 
483     let Some(krate) = def.krate(db) else { return Default::default() };
484     let Some(display_name) = krate.display_name(db) else { return Default::default() };
485     let crate_data = &db.crate_graph()[krate.into()];
486     let channel = crate_data.channel.map_or("nightly", ReleaseChannel::as_str);
487 
488     let (web_base, local_base) = match &crate_data.origin {
489         // std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself.
490         // FIXME: Use the toolchains channel instead of nightly
491         CrateOrigin::Lang(
492             origin @ (LangCrateOrigin::Alloc
493             | LangCrateOrigin::Core
494             | LangCrateOrigin::ProcMacro
495             | LangCrateOrigin::Std
496             | LangCrateOrigin::Test),
497         ) => {
498             let system_url = system_doc.and_then(|it| it.join(&format!("{origin}")).ok());
499             let web_url = format!("https://doc.rust-lang.org/{channel}/{origin}");
500             (Some(web_url), system_url)
501         }
502         CrateOrigin::Lang(_) => return (None, None),
503         CrateOrigin::Rustc { name: _ } => {
504             (Some(format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")), None)
505         }
506         CrateOrigin::Local { repo: _, name: _ } => {
507             // FIXME: These should not attempt to link to docs.rs!
508             let weblink = krate.get_html_root_url(db).or_else(|| {
509                 let version = krate.version(db);
510                 // Fallback to docs.rs. This uses `display_name` and can never be
511                 // correct, but that's what fallbacks are about.
512                 //
513                 // FIXME: clicking on the link should just open the file in the editor,
514                 // instead of falling back to external urls.
515                 Some(format!(
516                     "https://docs.rs/{krate}/{version}/",
517                     krate = display_name,
518                     version = version.as_deref().unwrap_or("*")
519                 ))
520             });
521             (weblink, local_doc)
522         }
523         CrateOrigin::Library { repo: _, name } => {
524             let weblink = krate.get_html_root_url(db).or_else(|| {
525                 let version = krate.version(db);
526                 // Fallback to docs.rs. This uses `display_name` and can never be
527                 // correct, but that's what fallbacks are about.
528                 //
529                 // FIXME: clicking on the link should just open the file in the editor,
530                 // instead of falling back to external urls.
531                 Some(format!(
532                     "https://docs.rs/{krate}/{version}/",
533                     krate = name,
534                     version = version.as_deref().unwrap_or("*")
535                 ))
536             });
537             (weblink, local_doc)
538         }
539     };
540     let web_base = web_base
541         .and_then(|it| Url::parse(&it).ok())
542         .and_then(|it| it.join(&format!("{display_name}/")).ok());
543     let local_base = local_base.and_then(|it| it.join(&format!("{display_name}/")).ok());
544 
545     (web_base, local_base)
546 }
547 
548 /// Get the filename and extension generated for a symbol by rustdoc.
549 ///
550 /// ```ignore
551 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
552 ///                                    ^^^^^^^^^^^^^^^^^^^
553 /// ```
filename_and_frag_for_def( db: &dyn HirDatabase, def: Definition, ) -> Option<(Definition, String, Option<String>)>554 fn filename_and_frag_for_def(
555     db: &dyn HirDatabase,
556     def: Definition,
557 ) -> Option<(Definition, String, Option<String>)> {
558     if let Some(assoc_item) = def.as_assoc_item(db) {
559         let def = match assoc_item.container(db) {
560             AssocItemContainer::Trait(t) => t.into(),
561             AssocItemContainer::Impl(i) => i.self_ty(db).as_adt()?.into(),
562         };
563         let (_, file, _) = filename_and_frag_for_def(db, def)?;
564         let frag = get_assoc_item_fragment(db, assoc_item)?;
565         return Some((def, file, Some(frag)));
566     }
567 
568     let res = match def {
569         Definition::Adt(adt) => match adt {
570             Adt::Struct(s) => format!("struct.{}.html", s.name(db).display(db.upcast())),
571             Adt::Enum(e) => format!("enum.{}.html", e.name(db).display(db.upcast())),
572             Adt::Union(u) => format!("union.{}.html", u.name(db).display(db.upcast())),
573         },
574         Definition::Module(m) => match m.name(db) {
575             // `#[doc(keyword = "...")]` is internal used only by rust compiler
576             Some(name) => match m.attrs(db).by_key("doc").find_string_value_in_tt("keyword") {
577                 Some(kw) => {
578                     format!("keyword.{}.html", kw.trim_matches('"'))
579                 }
580                 None => format!("{}/index.html", name.display(db.upcast())),
581             },
582             None => String::from("index.html"),
583         },
584         Definition::Trait(t) => format!("trait.{}.html", t.name(db).display(db.upcast())),
585         Definition::TraitAlias(t) => format!("traitalias.{}.html", t.name(db).display(db.upcast())),
586         Definition::TypeAlias(t) => format!("type.{}.html", t.name(db).display(db.upcast())),
587         Definition::BuiltinType(t) => format!("primitive.{}.html", t.name().display(db.upcast())),
588         Definition::Function(f) => format!("fn.{}.html", f.name(db).display(db.upcast())),
589         Definition::Variant(ev) => {
590             format!(
591                 "enum.{}.html#variant.{}",
592                 ev.parent_enum(db).name(db).display(db.upcast()),
593                 ev.name(db).display(db.upcast())
594             )
595         }
596         Definition::Const(c) => format!("const.{}.html", c.name(db)?.display(db.upcast())),
597         Definition::Static(s) => format!("static.{}.html", s.name(db).display(db.upcast())),
598         Definition::Macro(mac) => format!("macro.{}.html", mac.name(db).display(db.upcast())),
599         Definition::Field(field) => {
600             let def = match field.parent_def(db) {
601                 hir::VariantDef::Struct(it) => Definition::Adt(it.into()),
602                 hir::VariantDef::Union(it) => Definition::Adt(it.into()),
603                 hir::VariantDef::Variant(it) => Definition::Variant(it),
604             };
605             let (_, file, _) = filename_and_frag_for_def(db, def)?;
606             return Some((
607                 def,
608                 file,
609                 Some(format!("structfield.{}", field.name(db).display(db.upcast()))),
610             ));
611         }
612         Definition::SelfType(impl_) => {
613             let adt = impl_.self_ty(db).as_adt()?.into();
614             let (_, file, _) = filename_and_frag_for_def(db, adt)?;
615             // FIXME fragment numbering
616             return Some((adt, file, Some(String::from("impl"))));
617         }
618         Definition::Local(_)
619         | Definition::GenericParam(_)
620         | Definition::Label(_)
621         | Definition::BuiltinAttr(_)
622         | Definition::ToolModule(_)
623         | Definition::DeriveHelper(_) => return None,
624     };
625 
626     Some((def, res, None))
627 }
628 
629 /// Get the fragment required to link to a specific field, method, associated type, or associated constant.
630 ///
631 /// ```ignore
632 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
633 ///                                                       ^^^^^^^^^^^^^^
634 /// ```
get_assoc_item_fragment(db: &dyn HirDatabase, assoc_item: hir::AssocItem) -> Option<String>635 fn get_assoc_item_fragment(db: &dyn HirDatabase, assoc_item: hir::AssocItem) -> Option<String> {
636     Some(match assoc_item {
637         AssocItem::Function(function) => {
638             let is_trait_method =
639                 function.as_assoc_item(db).and_then(|assoc| assoc.containing_trait(db)).is_some();
640             // This distinction may get more complicated when specialization is available.
641             // Rustdoc makes this decision based on whether a method 'has defaultness'.
642             // Currently this is only the case for provided trait methods.
643             if is_trait_method && !function.has_body(db) {
644                 format!("tymethod.{}", function.name(db).display(db.upcast()))
645             } else {
646                 format!("method.{}", function.name(db).display(db.upcast()))
647             }
648         }
649         AssocItem::Const(constant) => {
650             format!("associatedconstant.{}", constant.name(db)?.display(db.upcast()))
651         }
652         AssocItem::TypeAlias(ty) => format!("associatedtype.{}", ty.name(db).display(db.upcast())),
653     })
654 }
655