Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/doc_links.rs')
| -rw-r--r-- | crates/ide/src/doc_links.rs | 112 |
1 files changed, 84 insertions, 28 deletions
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index 8d86c615d4..597b28d36d 100644 --- a/crates/ide/src/doc_links.rs +++ b/crates/ide/src/doc_links.rs @@ -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; @@ -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); @@ -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**. @@ -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 { @@ -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 @@ -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( @@ -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) { @@ -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) { @@ -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( @@ -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. @@ -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. @@ -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. |