1ef7a2329b
Abstract more over ItemTreeLoc-like structs Allows reducing some code duplication by using functions generic over said structs. The diff isn't negative due to me adding some additional impls for completeness.
687 lines
28 KiB
Rust
687 lines
28 KiB
Rust
//! Extracts, resolves and rewrites links and intra-doc links in markdown documentation.
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
mod intra_doc_links;
|
|
|
|
use std::ffi::OsStr;
|
|
|
|
use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
|
|
use pulldown_cmark_to_cmark::{cmark_resume_with_options, Options as CMarkOptions};
|
|
use stdx::format_to;
|
|
use url::Url;
|
|
|
|
use hir::{
|
|
db::HirDatabase, Adt, AsAssocItem, AssocItem, AssocItemContainer, DescendPreference, HasAttrs,
|
|
};
|
|
use ide_db::{
|
|
base_db::{CrateOrigin, LangCrateOrigin, ReleaseChannel, SourceDatabase},
|
|
defs::{Definition, NameClass, NameRefClass},
|
|
documentation::{docs_with_rangemap, Documentation, HasDocs},
|
|
helpers::pick_best_token,
|
|
RootDatabase,
|
|
};
|
|
use syntax::{
|
|
ast::{self, IsString},
|
|
match_ast, AstNode, AstToken,
|
|
SyntaxKind::*,
|
|
SyntaxNode, SyntaxToken, TextRange, TextSize, T,
|
|
};
|
|
|
|
use crate::{
|
|
doc_links::intra_doc_links::{parse_intra_doc_link, strip_prefixes_suffixes},
|
|
FilePosition, Semantics,
|
|
};
|
|
|
|
/// 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) -> String {
|
|
let mut cb = broken_link_clone_cb;
|
|
let doc = Parser::new_with_broken_link_callback(markdown, MARKDOWN_OPTIONS, Some(&mut cb));
|
|
|
|
let doc = map_links(doc, |target, title| {
|
|
// 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`
|
|
if let Some((target, title)) = rewrite_intra_doc_link(db, definition, target, title) {
|
|
(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<&OsStr>,
|
|
sysroot: Option<&OsStr>,
|
|
) -> Option<DocumentationLinks> {
|
|
let sema = &Semantics::new(db);
|
|
let file = sema.parse(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(DescendPreference::None, 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 } => {
|
|
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 } => 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>,
|
|
) -> Option<Definition> {
|
|
match def {
|
|
Definition::Module(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Function(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Adt(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Variant(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Const(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Static(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Trait(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::TraitAlias(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::TypeAlias(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::Field(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::SelfType(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::ExternCrateDecl(it) => it.resolve_doc_path(db, link, ns),
|
|
Definition::BuiltinAttr(_)
|
|
| Definition::ToolModule(_)
|
|
| Definition::BuiltinType(_)
|
|
| Definition::TupleField(_)
|
|
| Definition::Local(_)
|
|
| Definition::GenericParam(_)
|
|
| Definition::Label(_)
|
|
| Definition::DeriveHelper(_) => 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::Module(def))),
|
|
ast::Module(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Module(def))),
|
|
ast::Fn(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Function(def))),
|
|
ast::Struct(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Struct(def)))),
|
|
ast::Union(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Union(def)))),
|
|
ast::Enum(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Adt(hir::Adt::Enum(def)))),
|
|
ast::Variant(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Variant(def))),
|
|
ast::Trait(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Trait(def))),
|
|
ast::Static(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Static(def))),
|
|
ast::Const(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Const(def))),
|
|
ast::TypeAlias(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::TypeAlias(def))),
|
|
ast::Impl(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))),
|
|
ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
|
|
ast::TupleField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
|
|
ast::Macro(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(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(DescendPreference::None, doc_token).into_iter().find_map(|t| {
|
|
let (node, descended_prefix_len) = match_ast! {
|
|
match t {
|
|
ast::Comment(comment) => (t.parent()?, TextSize::try_from(comment.prefix().len()).ok()?),
|
|
ast::String(string) => (t.parent_ancestors().skip_while(|n| n.kind() != ATTR).nth(1)?, string.open_quote_text_range()?.len()),
|
|
_ => 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) = doc_attributes(sema, &node)?;
|
|
let (docs, doc_mapping) = docs_with_rangemap(sema.db, &attributes)?;
|
|
let (in_expansion_range, link, ns) =
|
|
extract_definitions_from_docs(&docs).into_iter().find_map(|(range, link, ns)| {
|
|
let mapped = doc_mapping.map(range)?;
|
|
(mapped.value.contains(abs_in_expansion_offset)).then_some((mapped.value, link, ns))
|
|
})?;
|
|
// 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)?;
|
|
cb(def, node, absolute_range)
|
|
})
|
|
}
|
|
}
|
|
|
|
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<&OsStr>,
|
|
sysroot: Option<&OsStr>,
|
|
) -> 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);
|
|
|
|
if 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,
|
|
) -> 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)?;
|
|
let mut url = get_doc_base_urls(db, resolved, None, None).0?;
|
|
|
|
let (_, file, _) = filename_and_frag_for_def(db, resolved)?;
|
|
if let Some(path) = mod_path_of_def(db, resolved) {
|
|
url = url.join(&path).ok()?;
|
|
}
|
|
|
|
url = url.join(&file).ok()?;
|
|
url.set_fragment(anchor);
|
|
|
|
Some((url.into(), strip_prefixes_suffixes(title).to_owned()))
|
|
}
|
|
|
|
/// 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.display(db)));
|
|
path
|
|
})
|
|
}
|
|
|
|
/// Rewrites a markdown document, applying 'callback' to each link.
|
|
fn map_links<'e>(
|
|
events: impl Iterator<Item = Event<'e>>,
|
|
callback: impl Fn(&str, &str) -> (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| 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.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);
|
|
end_link_target = Some(CowStr::Boxed(link_target_s.into()));
|
|
if !matches!(end_link_type, Some(LinkType::Autolink)) {
|
|
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);
|
|
end_link_target = Some(CowStr::Boxed(link_target_s.into()));
|
|
if !matches!(end_link_type, Some(LinkType::Autolink)) {
|
|
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<&OsStr>,
|
|
sysroot: Option<&OsStr>,
|
|
) -> (Option<Url>, Option<Url>) {
|
|
let local_doc = target_dir
|
|
.and_then(|path| path.to_str())
|
|
.and_then(|path| Url::parse(&format!("file:///{path}/")).ok())
|
|
.and_then(|it| it.join("doc/").ok());
|
|
let system_doc = sysroot
|
|
.and_then(|it| it.to_str())
|
|
.map(|sysroot| format!("file:///{sysroot}/share/doc/rust/html/"))
|
|
.and_then(|it| Url::parse(&it).ok());
|
|
|
|
// 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("https://doc.rust-lang.org/nightly/core/").ok();
|
|
let system_link = system_doc.and_then(|it| it.join("core/").ok());
|
|
return (web_link, system_link);
|
|
};
|
|
|
|
let Some(krate) = def.krate(db) else { return Default::default() };
|
|
let Some(display_name) = krate.display_name(db) else { return Default::default() };
|
|
let crate_data = &db.crate_graph()[krate.into()];
|
|
let channel = crate_data.channel().unwrap_or(ReleaseChannel::Nightly).as_str();
|
|
|
|
let (web_base, local_base) = match &crate_data.origin {
|
|
// 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).display(db.upcast())),
|
|
Adt::Enum(e) => format!("enum.{}.html", e.name(db).display(db.upcast())),
|
|
Adt::Union(u) => format!("union.{}.html", u.name(db).display(db.upcast())),
|
|
},
|
|
Definition::Module(m) => match m.name(db) {
|
|
// `#[doc(keyword = "...")]` is internal used only by rust compiler
|
|
Some(name) => match m.attrs(db).by_key("doc").find_string_value_in_tt("keyword") {
|
|
Some(kw) => {
|
|
format!("keyword.{}.html", kw.trim_matches('"'))
|
|
}
|
|
None => format!("{}/index.html", name.display(db.upcast())),
|
|
},
|
|
None => String::from("index.html"),
|
|
},
|
|
Definition::Trait(t) => format!("trait.{}.html", t.name(db).display(db.upcast())),
|
|
Definition::TraitAlias(t) => format!("traitalias.{}.html", t.name(db).display(db.upcast())),
|
|
Definition::TypeAlias(t) => format!("type.{}.html", t.name(db).display(db.upcast())),
|
|
Definition::BuiltinType(t) => format!("primitive.{}.html", t.name().display(db.upcast())),
|
|
Definition::Function(f) => format!("fn.{}.html", f.name(db).display(db.upcast())),
|
|
Definition::Variant(ev) => {
|
|
format!(
|
|
"enum.{}.html#variant.{}",
|
|
ev.parent_enum(db).name(db).display(db.upcast()),
|
|
ev.name(db).display(db.upcast())
|
|
)
|
|
}
|
|
Definition::Const(c) => format!("const.{}.html", c.name(db)?.display(db.upcast())),
|
|
Definition::Static(s) => format!("static.{}.html", s.name(db).display(db.upcast())),
|
|
Definition::Macro(mac) => match mac.kind(db) {
|
|
hir::MacroKind::Declarative
|
|
| hir::MacroKind::BuiltIn
|
|
| hir::MacroKind::Attr
|
|
| hir::MacroKind::ProcMacro => {
|
|
format!("macro.{}.html", mac.name(db).display(db.upcast()))
|
|
}
|
|
hir::MacroKind::Derive => {
|
|
format!("derive.{}.html", mac.name(db).display(db.upcast()))
|
|
}
|
|
},
|
|
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).display(db.upcast()))),
|
|
));
|
|
}
|
|
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).display(db.upcast()))
|
|
}
|
|
Definition::Local(_)
|
|
| Definition::GenericParam(_)
|
|
| Definition::TupleField(_)
|
|
| Definition::Label(_)
|
|
| Definition::BuiltinAttr(_)
|
|
| Definition::ToolModule(_)
|
|
| Definition::DeriveHelper(_) => 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).display(db.upcast()))
|
|
} else {
|
|
format!("method.{}", function.name(db).display(db.upcast()))
|
|
}
|
|
}
|
|
AssocItem::Const(constant) => {
|
|
format!("associatedconstant.{}", constant.name(db)?.display(db.upcast()))
|
|
}
|
|
AssocItem::TypeAlias(ty) => format!("associatedtype.{}", ty.name(db).display(db.upcast())),
|
|
})
|
|
}
|