mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-09-28 21:05:02 +00:00
Add support for local documentation links alongside web documentation links, pending for target_dir
path and tests
This commit is contained in:
parent
fc888b583d
commit
f2d933ecaf
4 changed files with 82 additions and 45 deletions
|
@ -29,8 +29,16 @@ use crate::{
|
||||||
FilePosition, Semantics,
|
FilePosition, Semantics,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Weblink to an item's documentation.
|
/// Web and local links to an item's documentation.
|
||||||
pub(crate) type DocumentationLink = String;
|
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DocumentationLinks {
|
||||||
|
/// The URL to the documentation on docs.rs.
|
||||||
|
/// Could be invalid.
|
||||||
|
pub web_url: Option<String>,
|
||||||
|
/// The URL to the documentation in the local file system.
|
||||||
|
/// Could be invalid.
|
||||||
|
pub local_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
const MARKDOWN_OPTIONS: Options =
|
const MARKDOWN_OPTIONS: Options =
|
||||||
Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS);
|
Options::ENABLE_FOOTNOTES.union(Options::ENABLE_TABLES).union(Options::ENABLE_TASKLISTS);
|
||||||
|
@ -119,10 +127,7 @@ pub(crate) fn remove_links(markdown: &str) -> String {
|
||||||
//
|
//
|
||||||
// | VS Code | **rust-analyzer: Open Docs**
|
// | VS Code | **rust-analyzer: Open Docs**
|
||||||
// |===
|
// |===
|
||||||
pub(crate) fn external_docs(
|
pub(crate) fn external_docs(db: &RootDatabase, position: &FilePosition) -> DocumentationLinks {
|
||||||
db: &RootDatabase,
|
|
||||||
position: &FilePosition,
|
|
||||||
) -> Option<DocumentationLink> {
|
|
||||||
let sema = &Semantics::new(db);
|
let sema = &Semantics::new(db);
|
||||||
let file = sema.parse(position.file_id).syntax().clone();
|
let file = sema.parse(position.file_id).syntax().clone();
|
||||||
let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
|
let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
|
||||||
|
@ -130,27 +135,30 @@ pub(crate) fn external_docs(
|
||||||
T!['('] | T![')'] => 2,
|
T!['('] | T![')'] => 2,
|
||||||
kind if kind.is_trivia() => 0,
|
kind if kind.is_trivia() => 0,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
})?;
|
});
|
||||||
|
let Some(token) = token else { return Default::default() };
|
||||||
let token = sema.descend_into_macros_single(token);
|
let token = sema.descend_into_macros_single(token);
|
||||||
|
|
||||||
let node = token.parent()?;
|
let Some(node) = token.parent() else { return Default::default() };
|
||||||
let definition = match_ast! {
|
let definition = match_ast! {
|
||||||
match node {
|
match node {
|
||||||
ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref)? {
|
ast::NameRef(name_ref) => match NameRefClass::classify(sema, &name_ref) {
|
||||||
NameRefClass::Definition(def) => def,
|
Some(NameRefClass::Definition(def)) => def,
|
||||||
NameRefClass::FieldShorthand { local_ref: _, field_ref } => {
|
Some(NameRefClass::FieldShorthand { local_ref: _, field_ref }) => {
|
||||||
Definition::Field(field_ref)
|
Definition::Field(field_ref)
|
||||||
}
|
}
|
||||||
|
None => return Default::default(),
|
||||||
},
|
},
|
||||||
ast::Name(name) => match NameClass::classify(sema, &name)? {
|
ast::Name(name) => match NameClass::classify(sema, &name) {
|
||||||
NameClass::Definition(it) | NameClass::ConstReference(it) => it,
|
Some(NameClass::Definition(it) | NameClass::ConstReference(it)) => it,
|
||||||
NameClass::PatFieldShorthand { local_def: _, field_ref } => Definition::Field(field_ref),
|
Some(NameClass::PatFieldShorthand { local_def: _, field_ref }) => Definition::Field(field_ref),
|
||||||
|
None => return Default::default(),
|
||||||
},
|
},
|
||||||
_ => return None,
|
_ => return Default::default(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
get_doc_link(db, definition)
|
return get_doc_links(db, definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts all links from a given markdown text returning the definition text range, link-text
|
/// 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<'_>)
|
||||||
//
|
//
|
||||||
// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
|
// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
|
||||||
// https://github.com/rust-lang/rfcs/pull/2988
|
// https://github.com/rust-lang/rfcs/pull/2988
|
||||||
fn get_doc_link(db: &RootDatabase, def: Definition) -> Option<String> {
|
fn get_doc_links(db: &RootDatabase, def: Definition) -> DocumentationLinks {
|
||||||
let (target, file, frag) = filename_and_frag_for_def(db, def)?;
|
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);
|
||||||
|
|
||||||
if let Some(path) = mod_path_of_def(db, target) {
|
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()?;
|
web_url = join_url(web_url, &file);
|
||||||
url.set_fragment(frag.as_deref());
|
local_url = join_url(local_url, &file);
|
||||||
|
|
||||||
Some(url.into())
|
set_fragment_for_url(web_url.as_mut(), frag.as_deref());
|
||||||
|
set_fragment_for_url(local_url.as_mut(), frag.as_deref());
|
||||||
|
|
||||||
|
return DocumentationLinks {
|
||||||
|
web_url: web_url.map(|it| it.into()),
|
||||||
|
local_url: local_url.map(|it| it.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
fn join_url(base_url: Option<Url>, path: &str) -> Option<Url> {
|
||||||
|
base_url.and_then(|url| url.join(path).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_fragment_for_url(url: Option<&mut Url>, frag: Option<&str>) {
|
||||||
|
url.map(|url| url.set_fragment(frag));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rewrite_intra_doc_link(
|
fn rewrite_intra_doc_link(
|
||||||
|
@ -332,7 +355,7 @@ fn rewrite_intra_doc_link(
|
||||||
let (link, ns) = parse_intra_doc_link(target);
|
let (link, ns) = parse_intra_doc_link(target);
|
||||||
|
|
||||||
let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
|
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).0?;
|
||||||
|
|
||||||
let (_, file, frag) = filename_and_frag_for_def(db, resolved)?;
|
let (_, file, frag) = filename_and_frag_for_def(db, resolved)?;
|
||||||
if let Some(path) = mod_path_of_def(db, resolved) {
|
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<
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut url = get_doc_base_url(db, def)?;
|
let mut url = get_doc_base_urls(db, def).0?;
|
||||||
let (def, file, frag) = filename_and_frag_for_def(db, def)?;
|
let (def, file, frag) = filename_and_frag_for_def(db, def)?;
|
||||||
|
|
||||||
if let Some(path) = mod_path_of_def(db, def) {
|
if let Some(path) = mod_path_of_def(db, def) {
|
||||||
|
@ -427,18 +450,26 @@ fn map_links<'e>(
|
||||||
/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
|
/// https://doc.rust-lang.org/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) -> (Option<Url>, Option<Url>) {
|
||||||
|
// TODO: get this is from `CargoWorkspace`
|
||||||
|
// TODO: get `CargoWorkspace` from `db`
|
||||||
|
let target_path = "file:///project/root/target";
|
||||||
|
let target_path = Url::parse(target_path).ok();
|
||||||
|
let local_doc_path = target_path.and_then(|url| url.join("doc").ok());
|
||||||
|
debug_assert!(local_doc_path.is_some(), "failed to parse local doc path");
|
||||||
|
|
||||||
// special case base url of `BuiltinType` to core
|
// special case base url of `BuiltinType` to core
|
||||||
// https://github.com/rust-lang/rust-analyzer/issues/12250
|
// https://github.com/rust-lang/rust-analyzer/issues/12250
|
||||||
if let Definition::BuiltinType(..) = def {
|
if let Definition::BuiltinType(..) = def {
|
||||||
return Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
|
let weblink = Url::parse("https://doc.rust-lang.org/nightly/core/").ok();
|
||||||
|
return (weblink, local_doc_path);
|
||||||
};
|
};
|
||||||
|
|
||||||
let krate = def.krate(db)?;
|
let Some(krate) = def.krate(db) else { return Default::default() };
|
||||||
let display_name = krate.display_name(db)?;
|
let Some(display_name) = krate.display_name(db) else { return Default::default() };
|
||||||
let crate_data = &db.crate_graph()[krate.into()];
|
let crate_data = &db.crate_graph()[krate.into()];
|
||||||
let channel = crate_data.channel.map_or("nightly", ReleaseChannel::as_str);
|
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.
|
// 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
|
// FIXME: Use the toolchains channel instead of nightly
|
||||||
CrateOrigin::Lang(
|
CrateOrigin::Lang(
|
||||||
|
@ -447,16 +478,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
|
||||||
| LangCrateOrigin::ProcMacro
|
| LangCrateOrigin::ProcMacro
|
||||||
| LangCrateOrigin::Std
|
| LangCrateOrigin::Std
|
||||||
| LangCrateOrigin::Test),
|
| LangCrateOrigin::Test),
|
||||||
) => {
|
) => (Some(format!("https://doc.rust-lang.org/{channel}/{origin}")), None),
|
||||||
format!("https://doc.rust-lang.org/{channel}/{origin}")
|
CrateOrigin::Lang(_) => return (None, None),
|
||||||
}
|
|
||||||
CrateOrigin::Lang(_) => return None,
|
|
||||||
CrateOrigin::Rustc { name: _ } => {
|
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: _ } => {
|
CrateOrigin::Local { repo: _, name: _ } => {
|
||||||
// FIXME: These should not attempt to link to docs.rs!
|
// 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);
|
let version = krate.version(db);
|
||||||
// Fallback to docs.rs. This uses `display_name` and can never be
|
// Fallback to docs.rs. This uses `display_name` and can never be
|
||||||
// correct, but that's what fallbacks are about.
|
// correct, but that's what fallbacks are about.
|
||||||
|
@ -468,10 +497,11 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
|
||||||
krate = display_name,
|
krate = display_name,
|
||||||
version = version.as_deref().unwrap_or("*")
|
version = version.as_deref().unwrap_or("*")
|
||||||
))
|
))
|
||||||
})?
|
});
|
||||||
|
(weblink, local_doc_path)
|
||||||
}
|
}
|
||||||
CrateOrigin::Library { repo: _, name } => {
|
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);
|
let version = krate.version(db);
|
||||||
// Fallback to docs.rs. This uses `display_name` and can never be
|
// Fallback to docs.rs. This uses `display_name` and can never be
|
||||||
// correct, but that's what fallbacks are about.
|
// correct, but that's what fallbacks are about.
|
||||||
|
@ -483,10 +513,14 @@ fn get_doc_base_url(db: &RootDatabase, def: Definition) -> Option<Url> {
|
||||||
krate = name,
|
krate = name,
|
||||||
version = version.as_deref().unwrap_or("*")
|
version = version.as_deref().unwrap_or("*")
|
||||||
))
|
))
|
||||||
})?
|
});
|
||||||
|
(weblink, local_doc_path)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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());
|
||||||
|
(web_base, local_base)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the filename and extension generated for a symbol by rustdoc.
|
/// Get the filename and extension generated for a symbol by rustdoc.
|
||||||
|
|
|
@ -471,7 +471,7 @@ impl Analysis {
|
||||||
pub fn external_docs(
|
pub fn external_docs(
|
||||||
&self,
|
&self,
|
||||||
position: FilePosition,
|
position: FilePosition,
|
||||||
) -> Cancellable<Option<doc_links::DocumentationLink>> {
|
) -> Cancellable<doc_links::DocumentationLinks> {
|
||||||
self.with_db(|db| doc_links::external_docs(db, &position))
|
self.with_db(|db| doc_links::external_docs(db, &position))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1535,13 +1535,16 @@ pub(crate) fn handle_semantic_tokens_range(
|
||||||
pub(crate) fn handle_open_docs(
|
pub(crate) fn handle_open_docs(
|
||||||
snap: GlobalStateSnapshot,
|
snap: GlobalStateSnapshot,
|
||||||
params: lsp_types::TextDocumentPositionParams,
|
params: lsp_types::TextDocumentPositionParams,
|
||||||
) -> Result<Option<lsp_types::Url>> {
|
) -> Result<(Option<lsp_types::Url>, Option<lsp_types::Url>)> {
|
||||||
let _p = profile::span("handle_open_docs");
|
let _p = profile::span("handle_open_docs");
|
||||||
let position = from_proto::file_position(&snap, params)?;
|
let position = from_proto::file_position(&snap, params)?;
|
||||||
|
|
||||||
let remote = snap.analysis.external_docs(position)?;
|
let Ok(remote_urls) = snap.analysis.external_docs(position) else { return Ok((None, None)); };
|
||||||
|
|
||||||
Ok(remote.and_then(|remote| Url::parse(&remote).ok()))
|
let web_url = remote_urls.web_url.and_then(|it| Url::parse(&it).ok());
|
||||||
|
let local_url = remote_urls.local_url.and_then(|it| Url::parse(&it).ok());
|
||||||
|
|
||||||
|
Ok((web_url, local_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_open_cargo_toml(
|
pub(crate) fn handle_open_cargo_toml(
|
||||||
|
|
|
@ -508,7 +508,7 @@ pub enum ExternalDocs {}
|
||||||
|
|
||||||
impl Request for ExternalDocs {
|
impl Request for ExternalDocs {
|
||||||
type Params = lsp_types::TextDocumentPositionParams;
|
type Params = lsp_types::TextDocumentPositionParams;
|
||||||
type Result = Option<lsp_types::Url>;
|
type Result = (Option<lsp_types::Url>, Option<lsp_types::Url>);
|
||||||
const METHOD: &'static str = "experimental/externalDocs";
|
const METHOD: &'static str = "experimental/externalDocs";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue