Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide links to locally built documentation for experimental/externalDocs #14662

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 84 additions & 28 deletions crates/ide/src/doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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;
Expand All @@ -29,8 +31,16 @@ use crate::{
FilePosition, Semantics,
};

/// Weblink to an item's documentation.
pub(crate) type DocumentationLink = String;
/// 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);
Expand Down Expand Up @@ -109,7 +119,7 @@ pub(crate) fn remove_links(markdown: &str) -> String {

// Feature: Open Docs
//
// Retrieve a link to documentation for the given symbol.
// 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**.
Expand All @@ -122,7 +132,9 @@ pub(crate) fn remove_links(markdown: &str) -> String {
pub(crate) fn external_docs(
db: &RootDatabase,
position: &FilePosition,
) -> Option<DocumentationLink> {
target_dir: Option<&OsStr>,
sysroot: Option<&OsStr>,
) -> Option<DocumentationLinks> {
let sema = &Semantics::new(db);
let file = sema.parse(position.file_id).syntax().clone();
let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
Expand All @@ -146,11 +158,11 @@ pub(crate) fn external_docs(
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref),
},
_ => return None,
_ => return None
}
};

get_doc_link(db, definition)
Some(get_doc_links(db, definition, target_dir, sysroot))
}

/// Extracts all links from a given markdown text returning the definition text range, link-text
Expand Down Expand Up @@ -308,19 +320,35 @@ fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)
//
// 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_link(db: &RootDatabase, def: Definition) -> Option<String> {
let (target, file, frag) = filename_and_frag_for_def(db, def)?;
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 url = get_doc_base_url(db, target)?;
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) {
url = url.join(&path).ok()?;
web_url = join_url(web_url, &path);
local_url = join_url(local_url, &path);
}

url = url.join(&file).ok()?;
url.set_fragment(frag.as_deref());
web_url = join_url(web_url, &file);
local_url = join_url(local_url, &file);

web_url.as_mut().map(|url| url.set_fragment(frag.as_deref()));
local_url.as_mut().map(|url| url.set_fragment(frag.as_deref()));

Some(url.into())
DocumentationLinks {
web_url: web_url.map(|it| it.into()),
local_url: local_url.map(|it| it.into()),
}
}

fn rewrite_intra_doc_link(
Expand All @@ -332,7 +360,7 @@ fn rewrite_intra_doc_link(
let (link, ns) = parse_intra_doc_link(target);

let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
let mut url = get_doc_base_url(db, resolved)?;
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) {
Expand All @@ -351,7 +379,7 @@ fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<
return None;
}

let mut url = get_doc_base_url(db, def)?;
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) {
Expand Down Expand Up @@ -426,19 +454,38 @@ fn map_links<'e>(
/// ```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_url(db: &RootDatabase, def: Definition) -> Option<Url> {
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 {
return Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
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 krate = def.krate(db)?;
let display_name = krate.display_name(db)?;
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.map_or("nightly", ReleaseChannel::as_str);
let base = match &crate_data.origin {

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(
Expand All @@ -448,15 +495,17 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
| LangCrateOrigin::Std
| LangCrateOrigin::Test),
) => {
format!("https://doc.rust-lang.org/{channel}/{origin}")
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,
CrateOrigin::Lang(_) => return (None, None),
CrateOrigin::Rustc { name: _ } => {
format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")
(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!
krate.get_html_root_url(db).or_else(|| {
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.
Expand All @@ -468,10 +517,11 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
krate = display_name,
version = version.as_deref().unwrap_or("*")
))
})?
});
(weblink, local_doc)
}
CrateOrigin::Library { repo: _, name } => {
krate.get_html_root_url(db).or_else(|| {
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.
Expand All @@ -483,10 +533,16 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
krate = name,
version = version.as_deref().unwrap_or("*")
))
})?
});
(weblink, local_doc)
}
};
Url::parse(&base).ok()?.join(&format!("{display_name}/")).ok()
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.
Expand Down
Loading