Skip to content

Commit c9b4116

Browse files
committed
Auto merge of #14662 - Ddystopia:open_locally_built_documentatin_instead_of_docs_dot_rs, r=Ddystopia
Provide links to locally built documentation for `experimental/externalDocs` This pull request addresses issue #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.
2 parents 4ecd7e6 + 2025f17 commit c9b4116

File tree

8 files changed

+307
-65
lines changed

8 files changed

+307
-65
lines changed

crates/ide/src/doc_links.rs

+84-28
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ mod tests;
55

66
mod intra_doc_links;
77

8+
use std::ffi::OsStr;
9+
810
use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
911
use pulldown_cmark_to_cmark::{cmark_resume_with_options, Options as CMarkOptions};
1012
use stdx::format_to;
@@ -29,8 +31,16 @@ use crate::{
2931
FilePosition, Semantics,
3032
};
3133

32-
/// Weblink to an item's documentation.
33-
pub(crate) type DocumentationLink = String;
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+
}
3444

3545
const MARKDOWN_OPTIONS: Options =
3646
Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS);
@@ -109,7 +119,7 @@ pub(crate) fn remove_links(markdown: &str) -> String {
109119

110120
// Feature: Open Docs
111121
//
112-
// Retrieve a link to documentation for the given symbol.
122+
// Retrieve a links to documentation for the given symbol.
113123
//
114124
// The simplest way to use this feature is via the context menu. Right-click on
115125
// the selected item. The context menu opens. Select **Open Docs**.
@@ -122,7 +132,9 @@ pub(crate) fn remove_links(markdown: &str) -> String {
122132
pub(crate) fn external_docs(
123133
db: &RootDatabase,
124134
position: &FilePosition,
125-
) -> Option<DocumentationLink> {
135+
target_dir: Option<&OsStr>,
136+
sysroot: Option<&OsStr>,
137+
) -> Option<DocumentationLinks> {
126138
let sema = &Semantics::new(db);
127139
let file = sema.parse(position.file_id).syntax().clone();
128140
let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
@@ -146,11 +158,11 @@ pub(crate) fn external_docs(
146158
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
147159
NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref),
148160
},
149-
_ => return None,
161+
_ => return None
150162
}
151163
};
152164

153-
get_doc_link(db, definition)
165+
Some(get_doc_links(db, definition, target_dir, sysroot))
154166
}
155167

156168
/// Extracts all links from a given markdown text returning the definition text range, link-text
@@ -308,19 +320,35 @@ fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)
308320
//
309321
// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
310322
// https://github.com/rust-lang/rfcs/pull/2988
311-
fn get_doc_link(db: &RootDatabase, def: Definition) -> Option<String> {
312-
let (target, file, frag) = filename_and_frag_for_def(db, def)?;
323+
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(); };
313334

314-
let mut url = get_doc_base_url(db, target)?;
335+
let (mut web_url, mut local_url) = get_doc_base_urls(db, target, target_dir, sysroot);
315336

316337
if let Some(path) = mod_path_of_def(db, target) {
317-
url = url.join(&path).ok()?;
338+
web_url = join_url(web_url, &path);
339+
local_url = join_url(local_url, &path);
318340
}
319341

320-
url = url.join(&file).ok()?;
321-
url.set_fragment(frag.as_deref());
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()));
322347

323-
Some(url.into())
348+
DocumentationLinks {
349+
web_url: web_url.map(|it| it.into()),
350+
local_url: local_url.map(|it| it.into()),
351+
}
324352
}
325353

326354
fn rewrite_intra_doc_link(
@@ -332,7 +360,7 @@ fn rewrite_intra_doc_link(
332360
let (link, ns) = parse_intra_doc_link(target);
333361

334362
let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
335-
let mut url = get_doc_base_url(db, resolved)?;
363+
let mut url = get_doc_base_urls(db, resolved, None, None).0?;
336364

337365
let (_, file, frag) = filename_and_frag_for_def(db, resolved)?;
338366
if let Some(path) = mod_path_of_def(db, resolved) {
@@ -351,7 +379,7 @@ fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<
351379
return None;
352380
}
353381

354-
let mut url = get_doc_base_url(db, def)?;
382+
let mut url = get_doc_base_urls(db, def, None, None).0?;
355383
let (def, file, frag) = filename_and_frag_for_def(db, def)?;
356384

357385
if let Some(path) = mod_path_of_def(db, def) {
@@ -426,19 +454,38 @@ fn map_links<'e>(
426454
/// ```ignore
427455
/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
428456
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^
457+
/// file:///project/root/target/doc/std/iter/trait.Iterator.html#tymethod.next
458+
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
429459
/// ```
430-
fn get_doc_base_url(db: &RootDatabase, def: Definition) -> 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+
431475
// special case base url of `BuiltinType` to core
432476
// https://github.com/rust-lang/rust-analyzer/issues/12250
433477
if let Definition::BuiltinType(..) = def {
434-
return Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
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);
435481
};
436482

437-
let krate = def.krate(db)?;
438-
let display_name = krate.display_name(db)?;
483+
let Some(krate) = def.krate(db) else { return Default::default() };
484+
let Some(display_name) = krate.display_name(db) else { return Default::default() };
439485
let crate_data = &db.crate_graph()[krate.into()];
440486
let channel = crate_data.channel.map_or("nightly", ReleaseChannel::as_str);
441-
let base = match &crate_data.origin {
487+
488+
let (web_base, local_base) = match &crate_data.origin {
442489
// std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself.
443490
// FIXME: Use the toolchains channel instead of nightly
444491
CrateOrigin::Lang(
@@ -448,15 +495,17 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
448495
| LangCrateOrigin::Std
449496
| LangCrateOrigin::Test),
450497
) => {
451-
format!("https://doc.rust-lang.org/{channel}/{origin}")
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)
452501
}
453-
CrateOrigin::Lang(_) => return None,
502+
CrateOrigin::Lang(_) => return (None, None),
454503
CrateOrigin::Rustc { name: _ } => {
455-
format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")
504+
(Some(format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")), None)
456505
}
457506
CrateOrigin::Local { repo: _, name: _ } => {
458507
// FIXME: These should not attempt to link to docs.rs!
459-
krate.get_html_root_url(db).or_else(|| {
508+
let weblink = krate.get_html_root_url(db).or_else(|| {
460509
let version = krate.version(db);
461510
// Fallback to docs.rs. This uses `display_name` and can never be
462511
// correct, but that's what fallbacks are about.
@@ -468,10 +517,11 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
468517
krate = display_name,
469518
version = version.as_deref().unwrap_or("*")
470519
))
471-
})?
520+
});
521+
(weblink, local_doc)
472522
}
473523
CrateOrigin::Library { repo: _, name } => {
474-
krate.get_html_root_url(db).or_else(|| {
524+
let weblink = krate.get_html_root_url(db).or_else(|| {
475525
let version = krate.version(db);
476526
// Fallback to docs.rs. This uses `display_name` and can never be
477527
// correct, but that's what fallbacks are about.
@@ -483,10 +533,16 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
483533
krate = name,
484534
version = version.as_deref().unwrap_or("*")
485535
))
486-
})?
536+
});
537+
(weblink, local_doc)
487538
}
488539
};
489-
Url::parse(&base).ok()?.join(&format!("{display_name}/")).ok()
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)
490546
}
491547

492548
/// Get the filename and extension generated for a symbol by rustdoc.

0 commit comments

Comments
 (0)