Skip to content

Commit 4d1039a

Browse files
committed
Add support for local documentation links alongside web documentation links, pending for target_dir path and tests
1 parent d31ddd5 commit 4d1039a

File tree

4 files changed

+82
-45
lines changed

4 files changed

+82
-45
lines changed

crates/ide/src/doc_links.rs

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ use crate::{
2929
FilePosition, Semantics,
3030
};
3131

32-
/// Weblink to an item's documentation.
33-
pub(crate) type DocumentationLink = String;
32+
/// Web and local links to an item's documentation.
33+
#[derive(Default, Debug, Clone, PartialEq, Eq)]
34+
pub struct DocumentationLinks {
35+
/// The URL to the documentation on docs.rs.
36+
/// Could be invalid.
37+
pub web_url: Option<String>,
38+
/// The URL to the documentation in the local file system.
39+
/// Could be invalid.
40+
pub local_url: Option<String>,
41+
}
3442

3543
const MARKDOWN_OPTIONS: Options =
3644
Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS);
@@ -119,38 +127,38 @@ pub(crate) fn remove_links(markdown: &str) -> String {
119127
//
120128
// | VS Code | **rust-analyzer: Open Docs**
121129
// |===
122-
pub(crate) fn external_docs(
123-
db: &RootDatabase,
124-
position: &FilePosition,
125-
) -> Option<DocumentationLink> {
130+
pub(crate) fn external_docs(db: &RootDatabase, position: &FilePosition) -> DocumentationLinks {
126131
let sema = &Semantics::new(db);
127132
let file = sema.parse(position.file_id).syntax().clone();
128133
let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
129134
IDENT | INT_NUMBER | T![self] => 3,
130135
T!['('] | T![')'] => 2,
131136
kind if kind.is_trivia() => 0,
132137
_ => 1,
133-
})?;
138+
});
139+
let Some(token) = token else { return Default::default() };
134140
let token = sema.descend_into_macros_single(token);
135141

136-
let node = token.parent()?;
142+
let Some(node) = token.parent() else { return Default::default() };
137143
let definition = match_ast! {
138144
match node {
139-
ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? {
140-
NameRefClass::Definition(def) => def,
141-
NameRefClass::FieldShorthand { local_ref: _, field_ref } => {
145+
ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref) {
146+
Some(NameRefClass::Definition(def)) => def,
147+
Some(NameRefClass::FieldShorthand { local_ref: _, field_ref }) => {
142148
Definition::Field(field_ref)
143149
}
150+
None => return Default::default(),
144151
},
145-
ast::Name(name) => match NameClass::classify(sema, &name)? {
146-
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
147-
NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref),
152+
ast::Name(name) => match NameClass::classify(sema, &name) {
153+
Some(NameClass::Definition(it) | NameClass::ConstReference(it)) => it,
154+
Some(NameClass::PatFieldShorthand { local_def: _, field_ref }) => Definition::Field(field_ref),
155+
None => return Default::default(),
148156
},
149-
_ => return None,
157+
_ => return Default::default(),
150158
}
151159
};
152160

153-
get_doc_link(db, definition)
161+
return get_doc_links(db, definition);
154162
}
155163

156164
/// Extracts all links from a given markdown text returning the definition text range, link-text
@@ -308,19 +316,34 @@ fn broken_link_clone_cb(link: BrokenLink<'_>) -> Option<(CowStr<'_>, CowStr<'_>)
308316
//
309317
// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
310318
// 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)?;
319+
fn get_doc_links(db: &RootDatabase, def: Definition) -> DocumentationLinks {
320+
let Some((target, file, frag)) = filename_and_frag_for_def(db, def) else { return Default::default(); };
313321

314-
let mut url = get_doc_base_url(db, target)?;
322+
let (mut web_url, mut local_url) = get_doc_base_urls(db, target);
315323

316324
if let Some(path) = mod_path_of_def(db, target) {
317-
url = url.join(&path).ok()?;
325+
web_url = join_url(web_url, &path);
326+
local_url = join_url(local_url, &path);
318327
}
319328

320-
url = url.join(&file).ok()?;
321-
url.set_fragment(frag.as_deref());
329+
web_url = join_url(web_url, &file);
330+
local_url = join_url(local_url, &file);
331+
332+
set_fragment_for_url(web_url.as_mut(), frag.as_deref());
333+
set_fragment_for_url(local_url.as_mut(), frag.as_deref());
322334

323-
Some(url.into())
335+
return DocumentationLinks {
336+
web_url: web_url.map(|it| it.into()),
337+
local_url: local_url.map(|it| it.into()),
338+
};
339+
340+
fn join_url(base_url: Option<Url>, path: &str) -> Option<Url> {
341+
base_url.and_then(|url| url.join(path).ok())
342+
}
343+
344+
fn set_fragment_for_url(url: Option<&mut Url>, frag: Option<&str>) {
345+
url.map(|url| url.set_fragment(frag));
346+
}
324347
}
325348

326349
fn rewrite_intra_doc_link(
@@ -332,7 +355,7 @@ fn rewrite_intra_doc_link(
332355
let (link, ns) = parse_intra_doc_link(target);
333356

334357
let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
335-
let mut url = get_doc_base_url(db, resolved)?;
358+
let mut url = get_doc_base_urls(db, resolved).0?;
336359

337360
let (_, file, frag) = filename_and_frag_for_def(db, resolved)?;
338361
if let Some(path) = mod_path_of_def(db, resolved) {
@@ -351,7 +374,7 @@ fn rewrite_url_link(db: &RootDatabase, def: Definition, target: &str) -> Option<
351374
return None;
352375
}
353376

354-
let mut url = get_doc_base_url(db, def)?;
377+
let mut url = get_doc_base_urls(db, def).0?;
355378
let (def, file, frag) = filename_and_frag_for_def(db, def)?;
356379

357380
if let Some(path) = mod_path_of_def(db, def) {
@@ -427,18 +450,26 @@ fn map_links<'e>(
427450
/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
428451
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^
429452
/// ```
430-
fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
453+
fn get_doc_base_urls(db: &RootDatabase, def: Definition) -> (Option<Url>, Option<Url>) {
454+
// TODO: get this is from `CargoWorkspace`
455+
// TODO: get `CargoWorkspace` from `db`
456+
let target_path = "file:///project/root/target";
457+
let target_path = Url::parse(target_path).ok();
458+
let local_doc_path = target_path.and_then(|url| url.join("doc").ok());
459+
debug_assert!(local_doc_path.is_some(), "failed to parse local doc path");
460+
431461
// special case base url of `BuiltinType` to core
432462
// https://github.com/rust-lang/rust-analyzer/issues/12250
433463
if let Definition::BuiltinType(..) = def {
434-
return Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
464+
let weblink = Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
465+
return (weblink, local_doc_path);
435466
};
436467

437-
let krate = def.krate(db)?;
438-
let display_name = krate.display_name(db)?;
468+
let Some(krate) = def.krate(db) else { return Default::default() };
469+
let Some(display_name) = krate.display_name(db) else { return Default::default() };
439470
let crate_data = &db.crate_graph()[krate.into()];
440471
let channel = crate_data.channel.map_or("nightly", ReleaseChannel::as_str);
441-
let base = match &crate_data.origin {
472+
let (web_base, local_base) = match &crate_data.origin {
442473
// std and co do not specify `html_root_url` any longer so we gotta handwrite this ourself.
443474
// FIXME: Use the toolchains channel instead of nightly
444475
CrateOrigin::Lang(
@@ -447,16 +478,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
447478
| LangCrateOrigin::ProcMacro
448479
| LangCrateOrigin::Std
449480
| LangCrateOrigin::Test),
450-
) => {
451-
format!("https://doc.rust-lang.org/{channel}/{origin}")
452-
}
453-
CrateOrigin::Lang(_) => return None,
481+
) => (Some(format!("https://doc.rust-lang.org/{channel}/{origin}")), None),
482+
CrateOrigin::Lang(_) => return (None, None),
454483
CrateOrigin::Rustc { name: _ } => {
455-
format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")
484+
(Some(format!("https://doc.rust-lang.org/{channel}/nightly-rustc/")), None)
456485
}
457486
CrateOrigin::Local { repo: _, name: _ } => {
458487
// FIXME: These should not attempt to link to docs.rs!
459-
krate.get_html_root_url(db).or_else(|| {
488+
let weblink = krate.get_html_root_url(db).or_else(|| {
460489
let version = krate.version(db);
461490
// Fallback to docs.rs. This uses `display_name` and can never be
462491
// correct, but that's what fallbacks are about.
@@ -468,10 +497,11 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
468497
krate = display_name,
469498
version = version.as_deref().unwrap_or("*")
470499
))
471-
})?
500+
});
501+
(weblink, local_doc_path)
472502
}
473503
CrateOrigin::Library { repo: _, name } => {
474-
krate.get_html_root_url(db).or_else(|| {
504+
let weblink = krate.get_html_root_url(db).or_else(|| {
475505
let version = krate.version(db);
476506
// Fallback to docs.rs. This uses `display_name` and can never be
477507
// correct, but that's what fallbacks are about.
@@ -483,10 +513,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
483513
krate = name,
484514
version = version.as_deref().unwrap_or("*")
485515
))
486-
})?
516+
});
517+
(weblink, local_doc_path)
487518
}
488519
};
489-
Url::parse(&base).ok()?.join(&format!("{display_name}/")).ok()
520+
let web_base = web_base
521+
.and_then(|it| Url::parse(&it).ok())
522+
.and_then(|it| it.join(&format!("{display_name}/")).ok());
523+
(web_base, local_base)
490524
}
491525

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

crates/ide/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ impl Analysis {
460460
pub fn external_docs(
461461
&self,
462462
position: FilePosition,
463-
) -> Cancellable<Option<doc_links::DocumentationLink>> {
463+
) -> Cancellable<doc_links::DocumentationLinks> {
464464
self.with_db(|db| doc_links::external_docs(db, &position))
465465
}
466466

crates/rust-analyzer/src/handlers.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,13 +1560,16 @@ pub(crate) fn handle_semantic_tokens_range(
15601560
pub(crate) fn handle_open_docs(
15611561
snap: GlobalStateSnapshot,
15621562
params: lsp_types::TextDocumentPositionParams,
1563-
) -> Result<Option<lsp_types::Url>> {
1563+
) -> Result<(Option<lsp_types::Url>, Option<lsp_types::Url>)> {
15641564
let _p = profile::span("handle_open_docs");
15651565
let position = from_proto::file_position(&snap, params)?;
15661566

1567-
let remote = snap.analysis.external_docs(position)?;
1567+
let Ok(remote_urls) = snap.analysis.external_docs(position) else { return Ok((None, None)); };
15681568

1569-
Ok(remote.and_then(|remote| Url::parse(&remote).ok()))
1569+
let web_url = remote_urls.web_url.and_then(|it| Url::parse(&it).ok());
1570+
let local_url = remote_urls.local_url.and_then(|it| Url::parse(&it).ok());
1571+
1572+
Ok((web_url, local_url))
15701573
}
15711574

15721575
pub(crate) fn handle_open_cargo_toml(

crates/rust-analyzer/src/lsp_ext.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ pub enum ExternalDocs {}
474474

475475
impl Request for ExternalDocs {
476476
type Params = lsp_types::TextDocumentPositionParams;
477-
type Result = Option<lsp_types::Url>;
477+
type Result = (Option<lsp_types::Url>, Option<lsp_types::Url>);
478478
const METHOD: &'static str = "experimental/externalDocs";
479479
}
480480

0 commit comments

Comments
 (0)