Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--crates/ide/src/doc_links.rs112
-rw-r--r--crates/ide/src/doc_links/tests.rs152
-rw-r--r--crates/ide/src/lib.rs15
-rw-r--r--crates/project-model/src/cargo_workspace.rs10
-rw-r--r--crates/rust-analyzer/src/config.rs4
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs37
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs16
-rw-r--r--docs/dev/lsp-extensions.md26
8 files changed, 307 insertions, 65 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.
diff --git a/crates/ide/src/doc_links/tests.rs b/crates/ide/src/doc_links/tests.rs
index b6b46c4508..05a64b33bf 100644
--- a/crates/ide/src/doc_links/tests.rs
+++ b/crates/ide/src/doc_links/tests.rs
@@ -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();
+
+ 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"),
+ }
- expect.assert_eq(&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) {
@@ -97,6 +121,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(
r#"
@@ -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,
)
}
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 24e2aed65a..1af4d39265 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -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.
diff --git a/crates/project-model/src/cargo_workspace.rs b/crates/project-model/src/cargo_workspace.rs
index fb98d61963..e821cae00a 100644
--- a/crates/project-model/src/cargo_workspace.rs
+++ b/crates/project-model/src/cargo_workspace.rs
@@ -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()
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index aa6beb6351..51874382a8 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -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")
}
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index f25dc74a14..3a208865a7 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -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(
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 69e7d82468..4d67c8b305 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -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 {
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index a4ad3e5a55..bc58aa7220 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -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. \ No newline at end of file
+Returns all crates from this workspace, so it can be used create a viewTree to help navigate the dependency tree.