Skip to content

Commit

Permalink
Auto merge of rust-lang#14662 - Ddystopia:open_locally_built_document…
Browse files Browse the repository at this point in the history
…atin_instead_of_docs_dot_rs, r=Ddystopia

 Provide links to locally built documentation for `experimental/externalDocs`

This pull request addresses issue rust-lang#12867, which requested the ability to provide links to locally built documentation when using the "Open docs for symbol" feature. Previously, rust-analyzer always used docs.rs for this purpose. With these changes, the feature will provide both web (docs.rs) and local documentation links without verifying their existence.

Changes in this PR:

   - Added support for local documentation links alongside web documentation links.
   - Added `target_dir` path argument for external_docs and other related methods.
   - Added `sysroot` argument for external_docs.
   - Added `target_directory` path to `CargoWorkspace`.

API Changes:

   - Added an experimental client capability `{ "localDocs": boolean }`. If this capability is set, the `Open External Documentation` request returned from the server will include both web and local documentation links in the `ExternalDocsResponse` object.

Here's the `ExternalDocsResponse` interface:

```typescript
interface ExternalDocsResponse {
    web?: string;
    local?: string;
}
```

By providing links to both web-based and locally built documentation, this update improves the developer experience for those using different versions of crates, git dependencies, or local crates not available on docs.rs. Rust-analyzer will now provide both web (docs.rs) and local documentation links, leaving it to the client to open the desired link. Please note that this update does not perform any checks to ensure the validity of the provided links.
  • Loading branch information
bors committed May 2, 2023
2 parents 4ecd7e6 + 2025f17 commit c9b4116
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 65 deletions.
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

0 comments on commit c9b4116

Please sign in to comment.