| //! Extracts, resolves and rewrites links and intra-doc links in markdown documentation. |
| |
| #[cfg(test)] |
| mod tests; |
| |
| mod intra_doc_links; |
| |
| use std::ops::Range; |
| |
| use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag}; |
| use pulldown_cmark_to_cmark::{Options as CMarkOptions, cmark_resume_with_options}; |
| use stdx::format_to; |
| use url::Url; |
| |
| use hir::{ |
| Adt, AsAssocItem, AssocItem, AssocItemContainer, AttrsWithOwner, HasAttrs, db::HirDatabase, sym, |
| }; |
| use ide_db::{ |
| RootDatabase, |
| base_db::{CrateOrigin, LangCrateOrigin, ReleaseChannel, RootQueryDb}, |
| defs::{Definition, NameClass, NameRefClass}, |
| documentation::{DocsRangeMap, Documentation, HasDocs, docs_with_rangemap}, |
| helpers::pick_best_token, |
| }; |
| use syntax::{ |
| AstNode, AstToken, |
| SyntaxKind::*, |
| SyntaxNode, SyntaxToken, T, TextRange, TextSize, |
| ast::{self, IsString}, |
| match_ast, |
| }; |
| |
| use crate::{ |
| FilePosition, Semantics, |
| doc_links::intra_doc_links::{parse_intra_doc_link, strip_prefixes_suffixes}, |
| }; |
| |
| /// Web and local links to an item's documentation. |
| #[derive(Default, Debug, Clone, PartialEq, Eq)] |
| pub struct DocumentationLinks { |
| /// The URL to the documentation on docs.rs. |
| /// May not lead anywhere. |
| pub web_url: Option<String>, |
| /// The URL to the documentation in the local file system. |
| /// May not lead anywhere. |
| pub local_url: Option<String>, |
| } |
| |
| const MARKDOWN_OPTIONS: Options = |
| Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS); |
| |
| /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) |
| pub(crate) fn rewrite_links( |
| db: &RootDatabase, |
| markdown: &str, |
| definition: Definition, |
| range_map: Option<DocsRangeMap>, |
| ) -> String { |
| let mut cb = broken_link_clone_cb; |
| let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb)) |
| .into_offset_iter(); |
| |
| let doc = map_links(doc, |target, title, range, link_type| { |
| // This check is imperfect, there's some overlap between valid intra-doc links |
| // and valid URLs so we choose to be too eager to try to resolve what might be |
| // a URL. |
| if target.contains("://") { |
| (Some(LinkType::Inline), target.to_owned(), title.to_owned()) |
| } else { |
| // Two possibilities: |
| // * path-based links: `../../module/struct.MyStruct.html` |
| // * module-based links (AKA intra-doc links): `super::super::module::MyStruct` |
| let text_range = |
| TextRange::new(range.start.try_into().unwrap(), range.end.try_into().unwrap()); |
| let is_inner_doc = range_map |
| .as_ref() |
| .and_then(|range_map| range_map.map(text_range)) |
| .map(|(_, attr_id)| attr_id.is_inner_attr()) |
| .unwrap_or(false); |
| if let Some((target, title)) = |
| rewrite_intra_doc_link(db, definition, target, title, is_inner_doc, link_type) |
| { |
| (None, target, title) |
| } else if let Some(target) = rewrite_url_link(db, definition, target) { |
| (Some(LinkType::Inline), target, title.to_owned()) |
| } else { |
| (None, target.to_owned(), title.to_owned()) |
| } |
| } |
| }); |
| let mut out = String::new(); |
| cmark_resume_with_options( |
| doc, |
| &mut out, |
| None, |
| CMarkOptions { code_block_token_count: 3, ..Default::default() }, |
| ) |
| .ok(); |
| out |
| } |
| |
| /// Remove all links in markdown documentation. |
| pub(crate) fn remove_links(markdown: &str) -> String { |
| let mut drop_link = false; |
| |
| let mut cb = |_: BrokenLink<'_>| { |
| let empty = InlineStr::try_from("").unwrap(); |
| Some((CowStr::Inlined(empty), CowStr::Inlined(empty))) |
| }; |
| let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb)); |
| let doc = doc.filter_map(move |evt| match evt { |
| Event::Start(Tag::Link(link_type, target, title)) => { |
| if link_type == LinkType::Inline && target.contains("://") { |
| Some(Event::Start(Tag::Link(link_type, target, title))) |
| } else { |
| drop_link = true; |
| None |
| } |
| } |
| Event::End(_) if drop_link => { |
| drop_link = false; |
| None |
| } |
| _ => Some(evt), |
| }); |
| |
| let mut out = String::new(); |
| cmark_resume_with_options( |
| doc, |
| &mut out, |
| None, |
| CMarkOptions { code_block_token_count: 3, ..Default::default() }, |
| ) |
| .ok(); |
| out |
| } |
| |
| // Feature: Open Docs |
| // |
| // Retrieve a links to documentation for the given symbol. |
| // |
| // The simplest way to use this feature is via the context menu. Right-click on |
| // the selected item. The context menu opens. Select **Open Docs**. |
| // |
| // | Editor | Action Name | |
| // |---------|-------------| |
| // | VS Code | **rust-analyzer: Open Docs** | |
| pub(crate) fn external_docs( |
| db: &RootDatabase, |
| FilePosition { file_id, offset }: FilePosition, |
| target_dir: Option<&str>, |
| sysroot: Option<&str>, |
| ) -> Option<DocumentationLinks> { |
| let sema = &Semantics::new(db); |
| let file = sema.parse_guess_edition(file_id).syntax().clone(); |
| let token = pick_best_token(file.token_at_offset(offset), |kind| match kind { |
| IDENT | INT_NUMBER | T![self] => 3, |
| T!['('] | T![')'] => 2, |
| kind if kind.is_trivia() => 0, |
| _ => 1, |
| })?; |
| let token = sema.descend_into_macros_single_exact(token); |
| |
| let node = token.parent()?; |
| let definition = match_ast! { |
| match node { |
| ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? { |
| NameRefClass::Definition(def, _) => def, |
| NameRefClass::FieldShorthand { local_ref: _, field_ref, adt_subst: _ } => { |
| Definition::Field(field_ref) |
| } |
| NameRefClass::ExternCrateShorthand { decl, .. } => { |
| Definition::ExternCrateDecl(decl) |
| } |
| }, |
| ast::Name(name) => match NameClass::classify(sema, &name)? { |
| NameClass::Definition(it) | NameClass::ConstReference(it) => it, |
| NameClass::PatFieldShorthand { local_def: _, field_ref, adt_subst: _ } => Definition::Field(field_ref), |
| }, |
| _ => return None |
| } |
| }; |
| |
| Some(get_doc_links(db, definition, target_dir, sysroot)) |
| } |
| |
| /// Extracts all links from a given markdown text returning the definition text range, link-text |
| /// and the namespace if known. |
| pub(crate) fn extract_definitions_from_docs( |
| docs: &Documentation, |
| ) -> Vec<(TextRange, String, Option<hir::Namespace>)> { |
| Parser::new_with_broken_link_callback( |
| docs.as_str(), |
| MARKDOWN_OPTIONS, |
| Some(&mut broken_link_clone_cb), |
| ) |
| .into_offset_iter() |
| .filter_map(|(event, range)| match event { |
| Event::Start(Tag::Link(_, target, _)) => { |
| let (link, ns) = parse_intra_doc_link(&target); |
| Some(( |
| TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?), |
| link.to_owned(), |
| ns, |
| )) |
| } |
| _ => None, |
| }) |
| .collect() |
| } |
| |
| pub(crate) fn resolve_doc_path_for_def( |
| db: &dyn HirDatabase, |
| def: Definition, |
| link: &str, |
| ns: Option<hir::Namespace>, |
| is_inner_doc: bool, |
| ) -> Option<Definition> { |
| match def { |
| Definition::Module(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Crate(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Function(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Adt(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Variant(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Const(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Static(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Trait(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::TypeAlias(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Macro(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::Field(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::SelfType(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::ExternCrateDecl(it) => it.resolve_doc_path(db, link, ns, is_inner_doc), |
| Definition::BuiltinAttr(_) |
| | Definition::BuiltinType(_) |
| | Definition::BuiltinLifetime(_) |
| | Definition::ToolModule(_) |
| | Definition::TupleField(_) |
| | Definition::Local(_) |
| | Definition::GenericParam(_) |
| | Definition::Label(_) |
| | Definition::DeriveHelper(_) |
| | Definition::InlineAsmRegOrRegClass(_) |
| | Definition::InlineAsmOperand(_) => None, |
| } |
| .map(Definition::from) |
| } |
| |
| pub(crate) fn doc_attributes( |
| sema: &Semantics<'_, RootDatabase>, |
| node: &SyntaxNode, |
| ) -> Option<(hir::AttrsWithOwner, Definition)> { |
| match_ast! { |
| match node { |
| ast::SourceFile(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(hir::Adt::Struct(def)))), |
| ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(hir::Adt::Union(def)))), |
| ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(hir::Adt::Enum(def)))), |
| ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| ast::ExternCrate(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::from(def))), |
| // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))), |
| _ => None |
| } |
| } |
| } |
| |
| pub(crate) struct DocCommentToken { |
| doc_token: SyntaxToken, |
| prefix_len: TextSize, |
| } |
| |
| pub(crate) fn token_as_doc_comment(doc_token: &SyntaxToken) -> Option<DocCommentToken> { |
| (match_ast! { |
| match doc_token { |
| ast::Comment(comment) => TextSize::try_from(comment.prefix().len()).ok(), |
| ast::String(string) => { |
| doc_token.parent_ancestors().find_map(ast::Attr::cast).filter(|attr| attr.simple_name().as_deref() == Some("doc"))?; |
| 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() { |
| return None; |
| } |
| string.open_quote_text_range().map(|it| it.len()) |
| }, |
| _ => None, |
| } |
| }).map(|prefix_len| DocCommentToken { prefix_len, doc_token: doc_token.clone() }) |
| } |
| |
| impl DocCommentToken { |
| pub(crate) fn get_definition_with_descend_at<T>( |
| self, |
| sema: &Semantics<'_, RootDatabase>, |
| offset: TextSize, |
| // Definition, CommentOwner, range of intra doc link in original file |
| mut cb: impl FnMut(Definition, SyntaxNode, TextRange) -> Option<T>, |
| ) -> Option<T> { |
| let DocCommentToken { prefix_len, doc_token } = self; |
| // offset relative to the comments contents |
| let original_start = doc_token.text_range().start(); |
| let relative_comment_offset = offset - original_start - prefix_len; |
| |
| sema.descend_into_macros(doc_token).into_iter().find_map(|t| { |
| let (node, descended_prefix_len, is_inner) = match_ast!{ |
| match t { |
| ast::Comment(comment) => { |
| (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?, comment.is_inner()) |
| }, |
| ast::String(string) => { |
| let attr = t.parent_ancestors().find_map(ast::Attr::cast)?; |
| let attr_is_inner = attr.excl_token().map(|excl| excl.kind() == BANG).unwrap_or(false); |
| (attr.syntax().parent()?, string.open_quote_text_range()?.len(), attr_is_inner) |
| }, |
| _ => return None, |
| } |
| }; |
| let token_start = t.text_range().start(); |
| let abs_in_expansion_offset = token_start + relative_comment_offset + descended_prefix_len; |
| let (attributes, def) = Self::doc_attributes(sema, &node, is_inner)?; |
| let (docs, doc_mapping) = docs_with_rangemap(sema.db, &attributes)?; |
| let (in_expansion_range, link, ns, is_inner) = |
| extract_definitions_from_docs(&docs).into_iter().find_map(|(range, link, ns)| { |
| let (mapped, idx) = doc_mapping.map(range)?; |
| (mapped.value.contains(abs_in_expansion_offset)).then_some((mapped.value, link, ns, idx.is_inner_attr())) |
| })?; |
| // get the relative range to the doc/attribute in the expansion |
| let in_expansion_relative_range = in_expansion_range - descended_prefix_len - token_start; |
| // Apply relative range to the original input comment |
| let absolute_range = in_expansion_relative_range + original_start + prefix_len; |
| let def = resolve_doc_path_for_def(sema.db, def, &link, ns, is_inner)?; |
| cb(def, node, absolute_range) |
| }) |
| } |
| |
| /// When we hover a inner doc item, this find a attached definition. |
| /// ``` |
| /// // node == ITEM_LIST |
| /// // node.parent == EXPR_BLOCK |
| /// // node.parent().parent() == FN |
| /// fn f() { |
| /// //! [`S$0`] |
| /// } |
| /// ``` |
| fn doc_attributes( |
| sema: &Semantics<'_, RootDatabase>, |
| node: &SyntaxNode, |
| is_inner_doc: bool, |
| ) -> Option<(AttrsWithOwner, Definition)> { |
| if is_inner_doc && node.kind() != SOURCE_FILE { |
| let parent = node.parent()?; |
| doc_attributes(sema, &parent).or(doc_attributes(sema, &parent.parent()?)) |
| } else { |
| doc_attributes(sema, node) |
| } |
| } |
| } |
| |
| fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)> { |
| Some((/*url*/ link.reference.clone(), /*title*/ link.reference)) |
| } |
| |
| // FIXME: |
| // BUG: For Option::Some |
| // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some |
| // Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html |
| // |
| // This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented |
| // https://github.com/rust-lang/rfcs/pull/2988 |
| fn get_doc_links( |
| db: &RootDatabase, |
| def: Definition, |
| target_dir: Option<&str>, |
| sysroot: Option<&str>, |
| ) -> DocumentationLinks { |
| let join_url = |base_url: Option<Url>, path: &str| -> Option<Url> { |
| base_url.and_then(|url| url.join(path).ok()) |
| }; |
| |
| let Some((target, file, frag)) = filename_and_frag_for_def(db, def) else { |
| return Default::default(); |
| }; |
| |
| let (mut web_url, mut local_url) = get_doc_base_urls(db, target, target_dir, sysroot); |
| |
| let append_mod = !matches!(def, Definition::Macro(m) if m.is_macro_export(db)); |
| if append_mod && let Some(path) = mod_path_of_def(db, target) { |
| web_url = join_url(web_url, &path); |
| local_url = join_url(local_url, &path); |
| } |
| |
| web_url = join_url(web_url, &file); |
| local_url = join_url(local_url, &file); |
| |
| if let Some(url) = web_url.as_mut() { |
| url.set_fragment(frag.as_deref()) |
| } |
| if let Some(url) = local_url.as_mut() { |
| url.set_fragment(frag.as_deref()) |
| } |
| |
| DocumentationLinks { |
| web_url: web_url.map(|it| it.into()), |
| local_url: local_url.map(|it| it.into()), |
| } |
| } |
| |
| fn rewrite_intra_doc_link( |
| db: &RootDatabase, |
| def: Definition, |
| target: &str, |
| title: &str, |
| is_inner_doc: bool, |
| link_type: LinkType, |
| ) -> Option<(String, String)> { |
| let (link, ns) = parse_intra_doc_link(target); |
| |
| let (link, anchor) = match link.split_once('#') { |
| Some((new_link, anchor)) => (new_link, Some(anchor)), |
| None => (link, None), |
| }; |
| |
| let resolved = resolve_doc_path_for_def(db, def, link, ns, is_inner_doc)?; |
| let mut url = get_doc_base_urls(db, resolved, None, None).0?; |
| |
| let (_, file, frag) = filename_and_frag_for_def(db, resolved)?; |
| if let Some(path) = mod_path_of_def(db, resolved) { |
| url = url.join(&path).ok()?; |
| } |
| |
| let frag = anchor.or(frag.as_deref()); |
| |
| url = url.join(&file).ok()?; |
| url.set_fragment(frag); |
| |
| // We want to strip the keyword prefix from the title, but only if the target is implicitly the same |
| // as the title. |
| let title = match link_type { |
| LinkType::Email |
| | LinkType::Autolink |
| | LinkType::Shortcut |
| | LinkType::Collapsed |
| | LinkType::Reference |
| | LinkType::Inline => title.to_owned(), |
| LinkType::ShortcutUnknown | LinkType::CollapsedUnknown | LinkType::ReferenceUnknown => { |
| strip_prefixes_suffixes(title).to_owned() |
| } |
| }; |
| |
| Some((url.into(), title)) |
| } |
| |
| /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`). |
| fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<String> { |
| if !(target.contains('#') || target.contains(".html")) { |
| return None; |
| } |
| |
| let mut url = get_doc_base_urls(db, def, None, None).0?; |
| let (def, file, frag) = filename_and_frag_for_def(db, def)?; |
| |
| if let Some(path) = mod_path_of_def(db, def) { |
| url = url.join(&path).ok()?; |
| } |
| |
| url = url.join(&file).ok()?; |
| url.set_fragment(frag.as_deref()); |
| url.join(target).ok().map(Into::into) |
| } |
| |
| fn mod_path_of_def(db: &RootDatabase, def: Definition) -> Option<String> { |
| def.canonical_module_path(db).map(|it| { |
| let mut path = String::new(); |
| it.flat_map(|it| it.name(db)).for_each(|name| format_to!(path, "{}/", name.as_str())); |
| path |
| }) |
| } |
| |
| /// Rewrites a markdown document, applying 'callback' to each link. |
| fn map_links<'e>( |
| events: impl Iterator<Item = (Event<'e>, Range<usize>)>, |
| callback: impl Fn(&str, &str, Range<usize>, LinkType) -> (Option<LinkType>, String, String), |
| ) -> impl Iterator<Item = Event<'e>> { |
| let mut in_link = false; |
| // holds the origin link target on start event and the rewritten one on end event |
| let mut end_link_target: Option<CowStr<'_>> = None; |
| // normally link's type is determined by the type of link tag in the end event, |
| // however in some cases we want to change the link type, for example, |
| // `Shortcut` type parsed from Start/End tags doesn't make sense for url links |
| let mut end_link_type: Option<LinkType> = None; |
| |
| events.map(move |(evt, range)| match evt { |
| Event::Start(Tag::Link(link_type, ref target, _)) => { |
| in_link = true; |
| end_link_target = Some(target.clone()); |
| end_link_type = Some(link_type); |
| evt |
| } |
| Event::End(Tag::Link(link_type, target, _)) => { |
| in_link = false; |
| Event::End(Tag::Link( |
| end_link_type.take().unwrap_or(link_type), |
| end_link_target.take().unwrap_or(target), |
| CowStr::Borrowed(""), |
| )) |
| } |
| Event::Text(s) if in_link => { |
| let (link_type, link_target_s, link_name) = |
| callback(&end_link_target.take().unwrap(), &s, range, end_link_type.unwrap()); |
| end_link_target = Some(CowStr::Boxed(link_target_s.into())); |
| if !matches!(end_link_type, Some(LinkType::Autolink)) && link_type.is_some() { |
| end_link_type = link_type; |
| } |
| Event::Text(CowStr::Boxed(link_name.into())) |
| } |
| Event::Code(s) if in_link => { |
| let (link_type, link_target_s, link_name) = |
| callback(&end_link_target.take().unwrap(), &s, range, end_link_type.unwrap()); |
| end_link_target = Some(CowStr::Boxed(link_target_s.into())); |
| if !matches!(end_link_type, Some(LinkType::Autolink)) && link_type.is_some() { |
| end_link_type = link_type; |
| } |
| Event::Code(CowStr::Boxed(link_name.into())) |
| } |
| _ => evt, |
| }) |
| } |
| |
| /// Get the root URL for the documentation of a definition. |
| /// |
| /// ```ignore |
| /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next |
| /// ^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| /// file:///project/root/target/doc/std/iter/trait.Iterator.html#tymethod.next |
| /// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| /// ``` |
| fn get_doc_base_urls( |
| db: &RootDatabase, |
| def: Definition, |
| target_dir: Option<&str>, |
| sysroot: Option<&str>, |
| ) -> (Option<Url>, Option<Url>) { |
| let local_doc = target_dir |
| .and_then(|path| Url::parse(&format!("file:///{path}/")).ok()) |
| .and_then(|it| it.join("doc/").ok()); |
| let system_doc = sysroot |
| .map(|sysroot| format!("file:///{sysroot}/share/doc/rust/html/")) |
| .and_then(|it| Url::parse(&it).ok()); |
| let krate = def.krate(db); |
| let channel = krate |
| .and_then(|krate| db.toolchain_channel(krate.into())) |
| .unwrap_or(ReleaseChannel::Nightly) |
| .as_str(); |
| |
| // special case base url of `BuiltinType` to core |
| // https://github.com/rust-lang/rust-analyzer/issues/12250 |
| if let Definition::BuiltinType(..) = def { |
| let web_link = Url::parse(&format!("https://doc.rust-lang.org/{channel}/core/")).ok(); |
| let system_link = system_doc.and_then(|it| it.join("core/").ok()); |
| return (web_link, system_link); |
| }; |
| |
| let Some(krate) = krate else { return Default::default() }; |
| let Some(display_name) = krate.display_name(db) else { return Default::default() }; |
| let (web_base, local_base) = match krate.origin(db) { |
| // std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself. |
| // FIXME: Use the toolchains channel instead of nightly |
| CrateOrigin::Lang( |
| origin @ (LangCrateOrigin::Alloc |
| | LangCrateOrigin::Core |
| | LangCrateOrigin::ProcMacro |
| | LangCrateOrigin::Std |
| | LangCrateOrigin::Test), |
| ) => { |
| let system_url = system_doc.and_then(|it| it.join(&format!("{origin}")).ok()); |
| let web_url = format!("https://doc.rust-lang.org/{channel}/{origin}"); |
| (Some(web_url), system_url) |
| } |
| CrateOrigin::Lang(_) => return (None, None), |
| CrateOrigin::Rustc { name: _ } => { |
| (Some(format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")), None) |
| } |
| CrateOrigin::Local { repo: _, name: _ } => { |
| // FIXME: These should not attempt to link to docs.rs! |
| let weblink = krate.get_html_root_url(db).or_else(|| { |
| let version = krate.version(db); |
| // Fallback to docs.rs. This uses `display_name` and can never be |
| // correct, but that's what fallbacks are about. |
| // |
| // FIXME: clicking on the link should just open the file in the editor, |
| // instead of falling back to external urls. |
| Some(format!( |
| "https://docs.rs/{krate}/{version}/", |
| krate = display_name, |
| version = version.as_deref().unwrap_or("*") |
| )) |
| }); |
| (weblink, local_doc) |
| } |
| CrateOrigin::Library { repo: _, name } => { |
| let weblink = krate.get_html_root_url(db).or_else(|| { |
| let version = krate.version(db); |
| // Fallback to docs.rs. This uses `display_name` and can never be |
| // correct, but that's what fallbacks are about. |
| // |
| // FIXME: clicking on the link should just open the file in the editor, |
| // instead of falling back to external urls. |
| Some(format!( |
| "https://docs.rs/{krate}/{version}/", |
| krate = name, |
| version = version.as_deref().unwrap_or("*") |
| )) |
| }); |
| (weblink, local_doc) |
| } |
| }; |
| let web_base = web_base |
| .and_then(|it| Url::parse(&it).ok()) |
| .and_then(|it| it.join(&format!("{display_name}/")).ok()); |
| let local_base = local_base.and_then(|it| it.join(&format!("{display_name}/")).ok()); |
| |
| (web_base, local_base) |
| } |
| |
| /// Get the filename and extension generated for a symbol by rustdoc. |
| /// |
| /// ```ignore |
| /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next |
| /// ^^^^^^^^^^^^^^^^^^^ |
| /// ``` |
| fn filename_and_frag_for_def( |
| db: &dyn HirDatabase, |
| def: Definition, |
| ) -> Option<(Definition, String, Option<String>)> { |
| if let Some(assoc_item) = def.as_assoc_item(db) { |
| let def = match assoc_item.container(db) { |
| AssocItemContainer::Trait(t) => t.into(), |
| AssocItemContainer::Impl(i) => i.self_ty(db).as_adt()?.into(), |
| }; |
| let (_, file, _) = filename_and_frag_for_def(db, def)?; |
| let frag = get_assoc_item_fragment(db, assoc_item)?; |
| return Some((def, file, Some(frag))); |
| } |
| |
| let res = match def { |
| Definition::Adt(adt) => match adt { |
| Adt::Struct(s) => { |
| format!("struct.{}.html", s.name(db).as_str()) |
| } |
| Adt::Enum(e) => format!("enum.{}.html", e.name(db).as_str()), |
| Adt::Union(u) => format!("union.{}.html", u.name(db).as_str()), |
| }, |
| Definition::Crate(_) => String::from("index.html"), |
| Definition::Module(m) => match m.name(db) { |
| // `#[doc(keyword = "...")]` is internal used only by rust compiler |
| Some(name) => { |
| match m.attrs(db).by_key(sym::doc).find_string_value_in_tt(sym::keyword) { |
| Some(kw) => { |
| format!("keyword.{kw}.html") |
| } |
| None => format!("{}/index.html", name.as_str()), |
| } |
| } |
| None => String::from("index.html"), |
| }, |
| Definition::Trait(t) => { |
| // FIXME(trait-alias): url should be traitalias. for aliases |
| format!("trait.{}.html", t.name(db).as_str()) |
| } |
| Definition::TypeAlias(t) => { |
| format!("type.{}.html", t.name(db).as_str()) |
| } |
| Definition::BuiltinType(t) => { |
| format!("primitive.{}.html", t.name().as_str()) |
| } |
| Definition::Function(f) => { |
| format!("fn.{}.html", f.name(db).as_str()) |
| } |
| Definition::Variant(ev) => { |
| let def = Definition::Adt(ev.parent_enum(db).into()); |
| let (_, file, _) = filename_and_frag_for_def(db, def)?; |
| return Some((def, file, Some(format!("variant.{}", ev.name(db).as_str())))); |
| } |
| Definition::Const(c) => { |
| format!("constant.{}.html", c.name(db)?.as_str()) |
| } |
| Definition::Static(s) => { |
| format!("static.{}.html", s.name(db).as_str()) |
| } |
| Definition::Macro(mac) => match mac.kind(db) { |
| hir::MacroKind::Declarative |
| | hir::MacroKind::AttrBuiltIn |
| | hir::MacroKind::DeclarativeBuiltIn |
| | hir::MacroKind::Attr |
| | hir::MacroKind::ProcMacro => { |
| format!("macro.{}.html", mac.name(db).as_str()) |
| } |
| hir::MacroKind::Derive | hir::MacroKind::DeriveBuiltIn => { |
| format!("derive.{}.html", mac.name(db).as_str()) |
| } |
| }, |
| Definition::Field(field) => { |
| let def = match field.parent_def(db) { |
| hir::VariantDef::Struct(it) => Definition::Adt(it.into()), |
| hir::VariantDef::Union(it) => Definition::Adt(it.into()), |
| hir::VariantDef::Variant(it) => Definition::Variant(it), |
| }; |
| let (_, file, _) = filename_and_frag_for_def(db, def)?; |
| return Some((def, file, Some(format!("structfield.{}", field.name(db).as_str())))); |
| } |
| Definition::SelfType(impl_) => { |
| let adt = impl_.self_ty(db).as_adt()?.into(); |
| let (_, file, _) = filename_and_frag_for_def(db, adt)?; |
| // FIXME fragment numbering |
| return Some((adt, file, Some(String::from("impl")))); |
| } |
| Definition::ExternCrateDecl(it) => { |
| format!("{}/index.html", it.name(db).as_str()) |
| } |
| Definition::Local(_) |
| | Definition::GenericParam(_) |
| | Definition::TupleField(_) |
| | Definition::Label(_) |
| | Definition::BuiltinAttr(_) |
| | Definition::BuiltinLifetime(_) |
| | Definition::ToolModule(_) |
| | Definition::DeriveHelper(_) |
| | Definition::InlineAsmRegOrRegClass(_) |
| | Definition::InlineAsmOperand(_) => return None, |
| }; |
| |
| Some((def, res, None)) |
| } |
| |
| /// Get the fragment required to link to a specific field, method, associated type, or associated constant. |
| /// |
| /// ```ignore |
| /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next |
| /// ^^^^^^^^^^^^^^ |
| /// ``` |
| fn get_assoc_item_fragment(db: &dyn HirDatabase, assoc_item: hir::AssocItem) -> Option<String> { |
| Some(match assoc_item { |
| AssocItem::Function(function) => { |
| let is_trait_method = |
| function.as_assoc_item(db).and_then(|assoc| assoc.container_trait(db)).is_some(); |
| // This distinction may get more complicated when specialization is available. |
| // Rustdoc makes this decision based on whether a method 'has defaultness'. |
| // Currently this is only the case for provided trait methods. |
| if is_trait_method && !function.has_body(db) { |
| format!("tymethod.{}", function.name(db).as_str()) |
| } else { |
| format!("method.{}", function.name(db).as_str()) |
| } |
| } |
| AssocItem::Const(constant) => { |
| format!("associatedconstant.{}", constant.name(db)?.as_str()) |
| } |
| AssocItem::TypeAlias(ty) => { |
| format!("associatedtype.{}", ty.name(db).as_str()) |
| } |
| }) |
| } |