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.
This commit is contained in:
commit
c9b4116a5e
@ -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 mut url = get_doc_base_url(db, target)?;
|
||||
let Some((target, file, frag)) = filename_and_frag_for_def(db, def) else { return Default::default(); };
|
||||
|
||||
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);
|
||||
|
||||
Some(url.into())
|
||||
web_url.as_mut().map(|url| url.set_fragment(frag.as_deref()));
|
||||
local_url.as_mut().map(|url| url.set_fragment(frag.as_deref()));
|
||||
|
||||
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.
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use expect_test::{expect, Expect};
|
||||
use hir::{HasAttrs, Semantics};
|
||||
use ide_db::{
|
||||
@ -13,11 +15,33 @@ use crate::{
|
||||
fixture, TryToNav,
|
||||
};
|
||||
|
||||
fn check_external_docs(ra_fixture: &str, expect: Expect) {
|
||||
fn check_external_docs(
|
||||
ra_fixture: &str,
|
||||
target_dir: Option<&OsStr>,
|
||||
expect_web_url: Option<Expect>,
|
||||
expect_local_url: Option<Expect>,
|
||||
sysroot: Option<&OsStr>,
|
||||
) {
|
||||
let (analysis, position) = fixture::position(ra_fixture);
|
||||
let url = analysis.external_docs(position).unwrap().expect("could not find url for symbol");
|
||||
let links = analysis.external_docs(position, target_dir, sysroot).unwrap();
|
||||
|
||||
expect.assert_eq(&url)
|
||||
let web_url = links.web_url;
|
||||
let local_url = links.local_url;
|
||||
|
||||
println!("web_url: {:?}", web_url);
|
||||
println!("local_url: {:?}", local_url);
|
||||
|
||||
match (expect_web_url, web_url) {
|
||||
(Some(expect), Some(url)) => expect.assert_eq(&url),
|
||||
(None, None) => (),
|
||||
_ => panic!("Unexpected web url"),
|
||||
}
|
||||
|
||||
match (expect_local_url, local_url) {
|
||||
(Some(expect), Some(url)) => expect.assert_eq(&url),
|
||||
(None, None) => (),
|
||||
_ => panic!("Unexpected local url"),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_rewrite(ra_fixture: &str, expect: Expect) {
|
||||
@ -96,6 +120,20 @@ fn node_to_def(
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_docs_doc_builtin_type() {
|
||||
check_external_docs(
|
||||
r#"
|
||||
//- /main.rs crate:foo
|
||||
let x: u3$02 = 0;
|
||||
"#,
|
||||
Some(&OsStr::new("/home/user/project")),
|
||||
Some(expect![[r#"https://doc.rust-lang.org/nightly/core/primitive.u32.html"#]]),
|
||||
Some(expect![[r#"file:///sysroot/share/doc/rust/html/core/primitive.u32.html"#]]),
|
||||
Some(&OsStr::new("/sysroot")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_docs_doc_url_crate() {
|
||||
check_external_docs(
|
||||
@ -105,7 +143,10 @@ use foo$0::Foo;
|
||||
//- /lib.rs crate:foo
|
||||
pub struct Foo;
|
||||
"#,
|
||||
expect![[r#"https://docs.rs/foo/*/foo/index.html"#]],
|
||||
Some(&OsStr::new("/home/user/project")),
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/index.html"#]]),
|
||||
Some(expect![[r#"file:///home/user/project/doc/foo/index.html"#]]),
|
||||
Some(&OsStr::new("/sysroot")),
|
||||
);
|
||||
}
|
||||
|
||||
@ -116,7 +157,10 @@ fn external_docs_doc_url_std_crate() {
|
||||
//- /main.rs crate:std
|
||||
use self$0;
|
||||
"#,
|
||||
expect!["https://doc.rust-lang.org/stable/std/index.html"],
|
||||
Some(&OsStr::new("/home/user/project")),
|
||||
Some(expect!["https://doc.rust-lang.org/stable/std/index.html"]),
|
||||
Some(expect!["file:///sysroot/share/doc/rust/html/std/index.html"]),
|
||||
Some(&OsStr::new("/sysroot")),
|
||||
);
|
||||
}
|
||||
|
||||
@ -127,7 +171,38 @@ fn external_docs_doc_url_struct() {
|
||||
//- /main.rs crate:foo
|
||||
pub struct Fo$0o;
|
||||
"#,
|
||||
expect![[r#"https://docs.rs/foo/*/foo/struct.Foo.html"#]],
|
||||
Some(&OsStr::new("/home/user/project")),
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/struct.Foo.html"#]]),
|
||||
Some(expect![[r#"file:///home/user/project/doc/foo/struct.Foo.html"#]]),
|
||||
Some(&OsStr::new("/sysroot")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_docs_doc_url_windows_backslash_path() {
|
||||
check_external_docs(
|
||||
r#"
|
||||
//- /main.rs crate:foo
|
||||
pub struct Fo$0o;
|
||||
"#,
|
||||
Some(&OsStr::new(r"C:\Users\user\project")),
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/struct.Foo.html"#]]),
|
||||
Some(expect![[r#"file:///C:/Users/user/project/doc/foo/struct.Foo.html"#]]),
|
||||
Some(&OsStr::new("/sysroot")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_docs_doc_url_windows_slash_path() {
|
||||
check_external_docs(
|
||||
r#"
|
||||
//- /main.rs crate:foo
|
||||
pub struct Fo$0o;
|
||||
"#,
|
||||
Some(&OsStr::new(r"C:/Users/user/project")),
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/struct.Foo.html"#]]),
|
||||
Some(expect![[r#"file:///C:/Users/user/project/doc/foo/struct.Foo.html"#]]),
|
||||
Some(&OsStr::new("/sysroot")),
|
||||
);
|
||||
}
|
||||
|
||||
@ -140,7 +215,10 @@ pub struct Foo {
|
||||
field$0: ()
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#structfield.field"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#structfield.field"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@ -151,7 +229,10 @@ fn external_docs_doc_url_fn() {
|
||||
//- /main.rs crate:foo
|
||||
pub fn fo$0o() {}
|
||||
"#,
|
||||
expect![[r#"https://docs.rs/foo/*/foo/fn.foo.html"#]],
|
||||
None,
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/fn.foo.html"#]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@ -165,7 +246,10 @@ impl Foo {
|
||||
pub fn method$0() {}
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#method.method"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#method.method"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
check_external_docs(
|
||||
r#"
|
||||
@ -175,7 +259,10 @@ impl Foo {
|
||||
const CONST$0: () = ();
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedconstant.CONST"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedconstant.CONST"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@ -192,7 +279,10 @@ impl Trait for Foo {
|
||||
pub fn method$0() {}
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#method.method"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#method.method"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
check_external_docs(
|
||||
r#"
|
||||
@ -205,7 +295,10 @@ impl Trait for Foo {
|
||||
const CONST$0: () = ();
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedconstant.CONST"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedconstant.CONST"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
check_external_docs(
|
||||
r#"
|
||||
@ -218,7 +311,10 @@ impl Trait for Foo {
|
||||
type Type$0 = ();
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedtype.Type"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/struct.Foo.html#associatedtype.Type"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@ -231,7 +327,10 @@ pub trait Foo {
|
||||
fn method$0();
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#tymethod.method"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#tymethod.method"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
check_external_docs(
|
||||
r#"
|
||||
@ -240,7 +339,10 @@ pub trait Foo {
|
||||
const CONST$0: ();
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#associatedconstant.CONST"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#associatedconstant.CONST"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
check_external_docs(
|
||||
r#"
|
||||
@ -249,7 +351,10 @@ pub trait Foo {
|
||||
type Type$0;
|
||||
}
|
||||
"#,
|
||||
expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#associatedtype.Type"##]],
|
||||
None,
|
||||
Some(expect![[r##"https://docs.rs/foo/*/foo/trait.Foo.html#associatedtype.Type"##]]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@ -260,7 +365,10 @@ fn external_docs_trait() {
|
||||
//- /main.rs crate:foo
|
||||
trait Trait$0 {}
|
||||
"#,
|
||||
expect![[r#"https://docs.rs/foo/*/foo/trait.Trait.html"#]],
|
||||
None,
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/trait.Trait.html"#]]),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@ -273,7 +381,10 @@ pub mod foo {
|
||||
pub mod ba$0r {}
|
||||
}
|
||||
"#,
|
||||
expect![[r#"https://docs.rs/foo/*/foo/foo/bar/index.html"#]],
|
||||
None,
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/foo/bar/index.html"#]]),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@ -294,7 +405,10 @@ fn foo() {
|
||||
let bar: wrapper::It$0em;
|
||||
}
|
||||
"#,
|
||||
expect![[r#"https://docs.rs/foo/*/foo/wrapper/module/struct.Item.html"#]],
|
||||
None,
|
||||
Some(expect![[r#"https://docs.rs/foo/*/foo/wrapper/module/struct.Item.html"#]]),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ mod view_item_tree;
|
||||
mod shuffle_crate_graph;
|
||||
mod fetch_crates;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{ffi::OsStr, sync::Arc};
|
||||
|
||||
use cfg::CfgOptions;
|
||||
use fetch_crates::CrateInfo;
|
||||
@ -467,12 +467,19 @@ impl Analysis {
|
||||
self.with_db(|db| moniker::moniker(db, position))
|
||||
}
|
||||
|
||||
/// Return URL(s) for the documentation of the symbol under the cursor.
|
||||
/// Returns URL(s) for the documentation of the symbol under the cursor.
|
||||
/// # Arguments
|
||||
/// * `position` - Position in the file.
|
||||
/// * `target_dir` - Directory where the build output is storeda.
|
||||
pub fn external_docs(
|
||||
&self,
|
||||
position: FilePosition,
|
||||
) -> Cancellable<Option<doc_links::DocumentationLink>> {
|
||||
self.with_db(|db| doc_links::external_docs(db, &position))
|
||||
target_dir: Option<&OsStr>,
|
||||
sysroot: Option<&OsStr>,
|
||||
) -> Cancellable<doc_links::DocumentationLinks> {
|
||||
self.with_db(|db| {
|
||||
doc_links::external_docs(db, &position, target_dir, sysroot).unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Computes parameter information at the given position.
|
||||
|
@ -32,6 +32,7 @@ pub struct CargoWorkspace {
|
||||
packages: Arena<PackageData>,
|
||||
targets: Arena<TargetData>,
|
||||
workspace_root: AbsPathBuf,
|
||||
target_directory: AbsPathBuf,
|
||||
}
|
||||
|
||||
impl ops::Index<Package> for CargoWorkspace {
|
||||
@ -414,7 +415,10 @@ impl CargoWorkspace {
|
||||
let workspace_root =
|
||||
AbsPathBuf::assert(PathBuf::from(meta.workspace_root.into_os_string()));
|
||||
|
||||
CargoWorkspace { packages, targets, workspace_root }
|
||||
let target_directory =
|
||||
AbsPathBuf::assert(PathBuf::from(meta.target_directory.into_os_string()));
|
||||
|
||||
CargoWorkspace { packages, targets, workspace_root, target_directory }
|
||||
}
|
||||
|
||||
pub fn packages(&self) -> impl Iterator<Item = Package> + ExactSizeIterator + '_ {
|
||||
@ -432,6 +436,10 @@ impl CargoWorkspace {
|
||||
&self.workspace_root
|
||||
}
|
||||
|
||||
pub fn target_directory(&self) -> &AbsPath {
|
||||
&self.target_directory
|
||||
}
|
||||
|
||||
pub fn package_flag(&self, package: &PackageData) -> String {
|
||||
if self.is_unique(&package.name) {
|
||||
package.name.clone()
|
||||
|
@ -1036,6 +1036,10 @@ impl Config {
|
||||
self.experimental("codeActionGroup")
|
||||
}
|
||||
|
||||
pub fn local_docs(&self) -> bool {
|
||||
self.experimental("localDocs")
|
||||
}
|
||||
|
||||
pub fn open_server_logs(&self) -> bool {
|
||||
self.experimental("openServerLogs")
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ use crate::{
|
||||
global_state::{GlobalState, GlobalStateSnapshot},
|
||||
line_index::LineEndings,
|
||||
lsp_ext::{
|
||||
self, CrateInfoResult, FetchDependencyListParams, FetchDependencyListResult,
|
||||
PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams,
|
||||
self, CrateInfoResult, ExternalDocsPair, ExternalDocsResponse, FetchDependencyListParams,
|
||||
FetchDependencyListResult, PositionOrRange, ViewCrateGraphParams, WorkspaceSymbolParams,
|
||||
},
|
||||
lsp_utils::{all_edits_are_disjoint, invalid_params_error},
|
||||
to_proto, LspError, Result,
|
||||
@ -1535,13 +1535,40 @@ pub(crate) fn handle_semantic_tokens_range(
|
||||
pub(crate) fn handle_open_docs(
|
||||
snap: GlobalStateSnapshot,
|
||||
params: lsp_types::TextDocumentPositionParams,
|
||||
) -> Result<Option<lsp_types::Url>> {
|
||||
) -> Result<ExternalDocsResponse> {
|
||||
let _p = profile::span("handle_open_docs");
|
||||
let position = from_proto::file_position(&snap, params)?;
|
||||
|
||||
let remote = snap.analysis.external_docs(position)?;
|
||||
let ws_and_sysroot = snap.workspaces.iter().find_map(|ws| match ws {
|
||||
ProjectWorkspace::Cargo { cargo, sysroot, .. } => Some((cargo, sysroot.as_ref().ok())),
|
||||
ProjectWorkspace::Json { .. } => None,
|
||||
ProjectWorkspace::DetachedFiles { .. } => None,
|
||||
});
|
||||
|
||||
Ok(remote.and_then(|remote| Url::parse(&remote).ok()))
|
||||
let (cargo, sysroot) = match ws_and_sysroot {
|
||||
Some((ws, sysroot)) => (Some(ws), sysroot),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let sysroot = sysroot.map(|p| p.root().as_os_str());
|
||||
let target_dir = cargo.map(|cargo| cargo.target_directory()).map(|p| p.as_os_str());
|
||||
|
||||
let Ok(remote_urls) = snap.analysis.external_docs(position, target_dir, sysroot) else {
|
||||
return if snap.config.local_docs() {
|
||||
Ok(ExternalDocsResponse::WithLocal(Default::default()))
|
||||
} else {
|
||||
Ok(ExternalDocsResponse::Simple(None))
|
||||
}
|
||||
};
|
||||
|
||||
let web = remote_urls.web_url.and_then(|it| Url::parse(&it).ok());
|
||||
let local = remote_urls.local_url.and_then(|it| Url::parse(&it).ok());
|
||||
|
||||
if snap.config.local_docs() {
|
||||
Ok(ExternalDocsResponse::WithLocal(ExternalDocsPair { web, local }))
|
||||
} else {
|
||||
Ok(ExternalDocsResponse::Simple(web))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_open_cargo_toml(
|
||||
|
@ -508,10 +508,24 @@ pub enum ExternalDocs {}
|
||||
|
||||
impl Request for ExternalDocs {
|
||||
type Params = lsp_types::TextDocumentPositionParams;
|
||||
type Result = Option<lsp_types::Url>;
|
||||
type Result = ExternalDocsResponse;
|
||||
const METHOD: &'static str = "experimental/externalDocs";
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum ExternalDocsResponse {
|
||||
Simple(Option<lsp_types::Url>),
|
||||
WithLocal(ExternalDocsPair),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExternalDocsPair {
|
||||
pub web: Option<lsp_types::Url>,
|
||||
pub local: Option<lsp_types::Url>,
|
||||
}
|
||||
|
||||
pub enum OpenCargoToml {}
|
||||
|
||||
impl Request for OpenCargoToml {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!---
|
||||
lsp_ext.rs hash: fdf1afd34548abbc
|
||||
lsp_ext.rs hash: 2d60bbffe70ae198
|
||||
|
||||
If you need to change the above hash to make the test pass, please check if you
|
||||
need to adjust this doc as well and ping this issue:
|
||||
@ -386,14 +386,26 @@ rust-analyzer supports only one `kind`, `"cargo"`. The `args` for `"cargo"` look
|
||||
|
||||
## Open External Documentation
|
||||
|
||||
This request is sent from client to server to get a URL to documentation for the symbol under the cursor, if available.
|
||||
This request is sent from the client to the server to obtain web and local URL(s) for documentation related to the symbol under the cursor, if available.
|
||||
|
||||
**Method** `experimental/externalDocs`
|
||||
**Method:** `experimental/externalDocs`
|
||||
|
||||
**Request:**: `TextDocumentPositionParams`
|
||||
**Request:** `TextDocumentPositionParams`
|
||||
|
||||
**Response** `string | null`
|
||||
**Response:** `string | null`
|
||||
|
||||
## Local Documentation
|
||||
|
||||
**Experimental Client Capability:** `{ "localDocs": boolean }`
|
||||
|
||||
If this capability is set, the `Open External Documentation` request returned from the server will have the following structure:
|
||||
|
||||
```typescript
|
||||
interface ExternalDocsResponse {
|
||||
web?: string;
|
||||
local?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Analyzer Status
|
||||
|
||||
@ -863,7 +875,7 @@ export interface Diagnostic {
|
||||
export interface FetchDependencyListParams {}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
**Response:**
|
||||
```typescript
|
||||
export interface FetchDependencyListResult {
|
||||
crates: {
|
||||
@ -873,4 +885,4 @@ export interface FetchDependencyListResult {
|
||||
}[];
|
||||
}
|
||||
```
|
||||
Returns all crates from this workspace, so it can be used create a viewTree to help navigate the dependency tree.
|
||||
Returns all crates from this workspace, so it can be used create a viewTree to help navigate the dependency tree.
|
||||
|
Loading…
x
Reference in New Issue
Block a user