diff options
| author | bors <bors@rust-lang.org> | 2023-05-02 15:57:19 +0000 |
|---|---|---|
| committer | bors <bors@rust-lang.org> | 2023-05-02 15:57:19 +0000 |
| commit | c9b4116a5e8ef956066c9aca51e3adbaa1647fcb (patch) | |
| tree | cd49d0fe266e0994954eeec93fbca057167782cb | |
| parent | 4ecd7e6c0d25ed9a83bf94e49c63d0b6919c22f0 (diff) | |
| parent | 2025f17ac394fc75528bb61422f15c5861a0fa8b (diff) | |
| download | rust-c9b4116a5e8ef956066c9aca51e3adbaa1647fcb.tar.gz rust-c9b4116a5e8ef956066c9aca51e3adbaa1647fcb.zip | |
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.
| -rw-r--r-- | crates/ide/src/doc_links.rs | 112 | ||||
| -rw-r--r-- | crates/ide/src/doc_links/tests.rs | 152 | ||||
| -rw-r--r-- | crates/ide/src/lib.rs | 15 | ||||
| -rw-r--r-- | crates/project-model/src/cargo_workspace.rs | 10 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/config.rs | 4 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/handlers/request.rs | 37 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lsp_ext.rs | 16 | ||||
| -rw-r--r-- | docs/dev/lsp-extensions.md | 26 |
8 files changed, 307 insertions, 65 deletions
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs index 8d86c615d44..597b28d36d7 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 b6b46c45088..05a64b33bfd 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 24e2aed65a5..1af4d39265f 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 fb98d61963c..e821cae00ac 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 aa6beb6351a..51874382a8b 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 f25dc74a142..3a208865a78 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 69e7d824680..4d67c8b305f 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 a4ad3e5a553..bc58aa7220d 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. |
