diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 3d932b931ad2068e079188fb72855956444f0a94..dcfc291968a36c50bd8f708bc7d1b5b909a22751 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -297,6 +297,7 @@ gpui::actions!( OpenExcerptsSplit, OpenProposedChangesEditor, OpenFile, + OpenDocs, OpenPermalinkToLine, OpenUrl, Outdent, diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index fa39e5c9d49ce9fd4415e2886ef26035f3cb002b..ba14f91ed29355b06b425dd6f31f0284847513a8 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -1,3 +1,5 @@ +use std::{fs, path::Path}; + use anyhow::Context as _; use gpui::{Context, View, ViewContext, VisualContext, WindowContext}; use language::Language; @@ -7,7 +9,7 @@ use text::ToPointUtf16; use crate::{ element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor, - ExpandMacroRecursively, + ExpandMacroRecursively, OpenDocs, }; const RUST_ANALYZER_NAME: &str = "rust-analyzer"; @@ -24,6 +26,7 @@ pub fn apply_related_actions(editor: &View, cx: &mut WindowContext) { .is_some() { register_action(editor, cx, expand_macro_recursively); + register_action(editor, cx, open_docs); } } @@ -94,3 +97,64 @@ pub fn expand_macro_recursively( }) .detach_and_log_err(cx); } + +pub fn open_docs(editor: &mut Editor, _: &OpenDocs, cx: &mut ViewContext<'_, Editor>) { + if editor.selections.count() == 0 { + return; + } + let Some(project) = &editor.project else { + return; + }; + let Some(workspace) = editor.workspace() else { + return; + }; + + let Some((trigger_anchor, _rust_language, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; + + let project = project.clone(); + let buffer_snapshot = buffer.read(cx).snapshot(); + let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot); + let open_docs_task = project.update(cx, |project, cx| { + project.request_lsp( + buffer, + project::LanguageServerToQuery::Other(server_to_query), + project::lsp_ext_command::OpenDocs { position }, + cx, + ) + }); + + cx.spawn(|_editor, mut cx| async move { + let docs_urls = open_docs_task.await.context("open docs")?; + if docs_urls.is_empty() { + log::debug!("Empty docs urls for position {position:?}"); + return Ok(()); + } else { + log::debug!("{:?}", docs_urls); + } + + workspace.update(&mut cx, |_workspace, cx| { + // Check if the local document exists, otherwise fallback to the online document. + // Open with the default browser. + if let Some(local_url) = docs_urls.local { + if fs::metadata(Path::new(&local_url[8..])).is_ok() { + cx.open_url(&local_url); + return; + } + } + + if let Some(web_url) = docs_urls.web { + cx.open_url(&web_url); + } + }) + }) + .detach_and_log_err(cx); +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ca09ef4d1fc24e27c868514e8e658cafaebb91a7..5f0186e61e87f4c15344c264aab518861a873141 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -762,6 +762,7 @@ impl LanguageServer { }), experimental: Some(json!({ "serverStatusNotification": true, + "localDocs": true, })), window: Some(WindowClientCapabilities { work_done_progress: Some(true), diff --git a/crates/project/src/lsp_ext_command.rs b/crates/project/src/lsp_ext_command.rs index 9fa1dc548077e641c0ecebcecd697c15ffc9edd7..7890630e315289449ef57262e2c98ff160fd6a0d 100644 --- a/crates/project/src/lsp_ext_command.rs +++ b/crates/project/src/lsp_ext_command.rs @@ -134,6 +134,132 @@ impl LspCommand for ExpandMacro { } } +pub enum LspOpenDocs {} + +impl lsp::request::Request for LspOpenDocs { + type Params = OpenDocsParams; + type Result = Option; + const METHOD: &'static str = "experimental/externalDocs"; +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct OpenDocsParams { + pub text_document: lsp::TextDocumentIdentifier, + pub position: lsp::Position, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct DocsUrls { + pub web: Option, + pub local: Option, +} + +impl DocsUrls { + pub fn is_empty(&self) -> bool { + self.web.is_none() && self.local.is_none() + } +} + +#[derive(Debug)] +pub struct OpenDocs { + pub position: PointUtf16, +} + +#[async_trait(?Send)] +impl LspCommand for OpenDocs { + type Response = DocsUrls; + type LspRequest = LspOpenDocs; + type ProtoRequest = proto::LspExtOpenDocs; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> OpenDocsParams { + OpenDocsParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: point_to_lsp(self.position), + } + } + + async fn response_from_lsp( + self, + message: Option, + _: Model, + _: Model, + _: LanguageServerId, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok(message + .map(|message| DocsUrls { + web: message.web, + local: message.local, + }) + .unwrap_or_default()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs { + proto::LspExtOpenDocs { + project_id, + buffer_id: buffer.remote_id().into(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + } + } + + async fn from_proto( + message: Self::ProtoRequest, + _: Model, + buffer: Model, + mut cx: AsyncAppContext, + ) -> anyhow::Result { + let position = message + .position + .and_then(deserialize_anchor) + .context("invalid position")?; + Ok(Self { + position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, + }) + } + + fn response_to_proto( + response: DocsUrls, + _: &mut LspStore, + _: PeerId, + _: &clock::Global, + _: &mut AppContext, + ) -> proto::LspExtOpenDocsResponse { + proto::LspExtOpenDocsResponse { + web: response.web, + local: response.local, + } + } + + async fn response_from_proto( + self, + message: proto::LspExtOpenDocsResponse, + _: Model, + _: Model, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok(DocsUrls { + web: message.web, + local: message.local, + }) + } + + fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result { + BufferId::new(message.buffer_id) + } +} + pub enum LspSwitchSourceHeader {} impl lsp::request::Request for LspSwitchSourceHeader { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index b86894bae1e361de6ade9988eeceae96c923bf08..dcd62751a719195059de31879fbd79f38ae9ff3a 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -276,6 +276,7 @@ message Envelope { LanguageServerPromptRequest language_server_prompt_request = 268; LanguageServerPromptResponse language_server_prompt_response = 269; + GitBranches git_branches = 270; GitBranchesResponse git_branches_response = 271; @@ -293,7 +294,10 @@ message Envelope { GetPanicFiles get_panic_files = 280; GetPanicFilesResponse get_panic_files_response = 281; - CancelLanguageServerWork cancel_language_server_work = 282; // current max + CancelLanguageServerWork cancel_language_server_work = 282; + + LspExtOpenDocs lsp_ext_open_docs = 283; + LspExtOpenDocsResponse lsp_ext_open_docs_response = 284; // current max } reserved 87 to 88; @@ -2024,6 +2028,17 @@ message LspExtExpandMacroResponse { string expansion = 2; } +message LspExtOpenDocs { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; +} + +message LspExtOpenDocsResponse { + optional string web = 1; + optional string local = 2; +} + message LspExtSwitchSourceHeader { uint64 project_id = 1; uint64 buffer_id = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index e143f24a47a5a375ca0d05eb448c62c3fd13daf5..2ec9f8bf55d200f7ee4496a6154fa827d5c23e87 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -314,6 +314,8 @@ messages!( (UsersResponse, Foreground), (LspExtExpandMacro, Background), (LspExtExpandMacroResponse, Background), + (LspExtOpenDocs, Background), + (LspExtOpenDocsResponse, Background), (SetRoomParticipantRole, Foreground), (BlameBuffer, Foreground), (BlameBufferResponse, Foreground), @@ -464,6 +466,7 @@ request_messages!( (UpdateProject, Ack), (UpdateWorktree, Ack), (LspExtExpandMacro, LspExtExpandMacroResponse), + (LspExtOpenDocs, LspExtOpenDocsResponse), (SetRoomParticipantRole, Ack), (BlameBuffer, BlameBufferResponse), (RejoinRemoteProjects, RejoinRemoteProjectsResponse), @@ -552,6 +555,7 @@ entity_messages!( UpdateWorktree, UpdateWorktreeSettings, LspExtExpandMacro, + LspExtOpenDocs, AdvertiseContexts, OpenContext, CreateContext,