diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index b8c616af82..1e68f818bd 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -306,6 +306,7 @@ gpui::actions!( SortLinesCaseInsensitive, SortLinesCaseSensitive, SplitSelectionIntoLines, + SwitchSourceHeader, Tab, TabPrev, ToggleAutoSignatureHelp, diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs new file mode 100644 index 0000000000..74a3b20449 --- /dev/null +++ b/crates/editor/src/clangd_ext.rs @@ -0,0 +1,93 @@ +use std::path::PathBuf; + +use anyhow::Context as _; +use gpui::{View, ViewContext, WindowContext}; +use language::Language; +use url::Url; + +use crate::lsp_ext::find_specific_language_server_in_selection; + +use crate::{element::register_action, Editor, SwitchSourceHeader}; + +static CLANGD_SERVER_NAME: &str = "clangd"; + +fn is_c_language(language: &Language) -> bool { + return language.name().as_ref() == "C++" || language.name().as_ref() == "C"; +} + +pub fn switch_source_header( + editor: &mut Editor, + _: &SwitchSourceHeader, + cx: &mut ViewContext<'_, Editor>, +) { + let Some(project) = &editor.project else { + return; + }; + let Some(workspace) = editor.workspace() else { + return; + }; + + let Some((_, _, server_to_query, buffer)) = + find_specific_language_server_in_selection(&editor, cx, &is_c_language, CLANGD_SERVER_NAME) + else { + return; + }; + + let project = project.clone(); + let buffer_snapshot = buffer.read(cx).snapshot(); + let source_file = buffer_snapshot + .file() + .unwrap() + .file_name(cx) + .to_str() + .unwrap() + .to_owned(); + + let switch_source_header_task = project.update(cx, |project, cx| { + project.request_lsp( + buffer, + project::LanguageServerToQuery::Other(server_to_query), + project::lsp_ext_command::SwitchSourceHeader, + cx, + ) + }); + cx.spawn(|_editor, mut cx| async move { + let switch_source_header = switch_source_header_task + .await + .with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?; + if switch_source_header.0.is_empty() { + log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file); + return Ok(()); + } + + let goto = Url::parse(&switch_source_header.0).with_context(|| { + format!( + "Parsing URL \"{}\" returned from switch source/header failed", + switch_source_header.0 + ) + })?; + + workspace + .update(&mut cx, |workspace, view_cx| { + workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx) + }) + .with_context(|| { + format!( + "Switch source/header could not open \"{}\" in workspace", + goto.path() + ) + })? + .await + .map(|_| ()) + }) + .detach_and_log_err(cx); +} + +pub fn apply_related_actions(editor: &View, cx: &mut WindowContext) { + if editor.update(cx, |e, cx| { + find_specific_language_server_in_selection(e, cx, &is_c_language, CLANGD_SERVER_NAME) + .is_some() + }) { + register_action(editor, cx, switch_source_header); + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a83553565e..bd147b7a47 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -15,6 +15,7 @@ pub mod actions; mod blame_entry_tooltip; mod blink_manager; +mod clangd_ext; mod debounced_delay; pub mod display_map; mod editor_settings; @@ -30,6 +31,7 @@ mod inlay_hint_cache; mod inline_completion_provider; pub mod items; mod linked_editing_ranges; +mod lsp_ext; mod mouse_context_menu; pub mod movement; mod persistence; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4867210cbe..f26765a574 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -165,6 +165,7 @@ impl EditorElement { }); crate::rust_analyzer_ext::apply_related_actions(view, cx); + crate::clangd_ext::apply_related_actions(view, cx); register_action(view, cx, Editor::move_left); register_action(view, cx, Editor::move_right); register_action(view, cx, Editor::move_down); diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs new file mode 100644 index 0000000000..93424ea567 --- /dev/null +++ b/crates/editor/src/lsp_ext.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use crate::Editor; +use gpui::{Model, WindowContext}; +use language::Buffer; +use language::Language; +use lsp::LanguageServerId; +use multi_buffer::Anchor; + +pub(crate) fn find_specific_language_server_in_selection( + editor: &Editor, + cx: &WindowContext, + filter_language: F, + language_server_name: &str, +) -> Option<(Anchor, Arc, LanguageServerId, Model)> +where + F: Fn(&Language) -> bool, +{ + let Some(project) = &editor.project else { + return None; + }; + let multibuffer = editor.buffer().read(cx); + editor + .selections + .disjoint_anchors() + .into_iter() + .filter(|selection| selection.start == selection.end) + .filter_map(|selection| Some((selection.start.buffer_id?, selection.start))) + .filter_map(|(buffer_id, trigger_anchor)| { + let buffer = multibuffer.buffer(buffer_id)?; + let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?; + if !filter_language(&language) { + return None; + } + Some((trigger_anchor, language, buffer)) + }) + .find_map(|(trigger_anchor, language, buffer)| { + project + .read(cx) + .language_servers_for_buffer(buffer.read(cx), cx) + .find_map(|(adapter, server)| { + if adapter.name.0.as_ref() == language_server_name { + Some(( + trigger_anchor, + Arc::clone(&language), + server.server_id(), + buffer.clone(), + )) + } else { + None + } + }) + }) +} diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 9a4dfbdf2a..d97bf8ad15 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use anyhow::Context as _; use gpui::{Context, View, ViewContext, VisualContext, WindowContext}; use language::Language; @@ -7,22 +5,24 @@ use multi_buffer::MultiBuffer; use project::lsp_ext_command::ExpandMacro; use text::ToPointUtf16; -use crate::{element::register_action, Editor, ExpandMacroRecursively}; +use crate::{ + element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor, + ExpandMacroRecursively, +}; + +static RUST_ANALYZER_NAME: &str = "rust-analyzer"; + +fn is_rust_language(language: &Language) -> bool { + language.name().as_ref() == "Rust" +} pub fn apply_related_actions(editor: &View, cx: &mut WindowContext) { - let is_rust_related = editor.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .all_buffers() - .iter() - .any(|b| match b.read(cx).language() { - Some(l) => is_rust_language(l), - None => false, - }) - }); - - if is_rust_related { + if editor + .update(cx, |e, cx| { + find_specific_language_server_in_selection(e, cx, &is_rust_language, RUST_ANALYZER_NAME) + }) + .is_some() + { register_action(editor, cx, expand_macro_recursively); } } @@ -42,39 +42,13 @@ pub fn expand_macro_recursively( return; }; - let multibuffer = editor.buffer().read(cx); - - let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor - .selections - .disjoint_anchors() - .into_iter() - .filter(|selection| selection.start == selection.end) - .filter_map(|selection| Some((selection.start.buffer_id?, selection.start))) - .filter_map(|(buffer_id, trigger_anchor)| { - let buffer = multibuffer.buffer(buffer_id)?; - let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?; - if !is_rust_language(&rust_language) { - return None; - } - Some((trigger_anchor, rust_language, buffer)) - }) - .find_map(|(trigger_anchor, rust_language, buffer)| { - project - .read(cx) - .language_servers_for_buffer(buffer.read(cx), cx) - .find_map(|(adapter, server)| { - if adapter.name.0.as_ref() == "rust-analyzer" { - Some(( - trigger_anchor, - Arc::clone(&rust_language), - server.server_id(), - buffer.clone(), - )) - } else { - None - } - }) - }) + 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; }; @@ -120,7 +94,3 @@ pub fn expand_macro_recursively( }) .detach_and_log_err(cx); } - -fn is_rust_language(language: &Language) -> bool { - language.name().as_ref() == "Rust" -} diff --git a/crates/project/src/lsp_ext_command.rs b/crates/project/src/lsp_ext_command.rs index a35fdd152f..0d5c495e32 100644 --- a/crates/project/src/lsp_ext_command.rs +++ b/crates/project/src/lsp_ext_command.rs @@ -135,3 +135,97 @@ impl LspCommand for ExpandMacro { BufferId::new(message.buffer_id) } } + +pub enum LspSwitchSourceHeader {} + +impl lsp::request::Request for LspSwitchSourceHeader { + type Params = SwitchSourceHeaderParams; + type Result = Option; + const METHOD: &'static str = "textDocument/switchSourceHeader"; +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier); + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct SwitchSourceHeaderResult(pub String); + +#[derive(Default, Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SwitchSourceHeader; + +#[async_trait(?Send)] +impl LspCommand for SwitchSourceHeader { + type Response = SwitchSourceHeaderResult; + type LspRequest = LspSwitchSourceHeader; + type ProtoRequest = proto::LspExtSwitchSourceHeader; + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &AppContext, + ) -> SwitchSourceHeaderParams { + SwitchSourceHeaderParams(lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }) + } + + async fn response_from_lsp( + self, + message: Option, + _: Model, + _: Model, + _: LanguageServerId, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok(message + .map(|message| SwitchSourceHeaderResult(message.0)) + .unwrap_or_default()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtSwitchSourceHeader { + proto::LspExtSwitchSourceHeader { + project_id, + buffer_id: buffer.remote_id().into(), + } + } + + async fn from_proto( + _: Self::ProtoRequest, + _: Model, + _: Model, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok(Self {}) + } + + fn response_to_proto( + response: SwitchSourceHeaderResult, + _: &mut Project, + _: PeerId, + _: &clock::Global, + _: &mut AppContext, + ) -> proto::LspExtSwitchSourceHeaderResponse { + proto::LspExtSwitchSourceHeaderResponse { + target_file: response.0, + } + } + + async fn response_from_proto( + self, + message: proto::LspExtSwitchSourceHeaderResponse, + _: Model, + _: Model, + _: AsyncAppContext, + ) -> anyhow::Result { + Ok(SwitchSourceHeaderResult(message.target_file)) + } + + fn buffer_id_from_proto(message: &proto::LspExtSwitchSourceHeader) -> Result { + BufferId::new(message.buffer_id) + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index a78f6dca07..1de83748c2 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -131,7 +131,7 @@ message Envelope { UpdateUserPlan update_user_plan = 234; UpdateDiffBase update_diff_base = 104; AcceptTermsOfService accept_terms_of_service = 239; - AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; // current max + AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; OnTypeFormatting on_type_formatting = 105; OnTypeFormattingResponse on_type_formatting_response = 106; @@ -264,15 +264,18 @@ message Envelope { GetSignatureHelp get_signature_help = 217; GetSignatureHelpResponse get_signature_help_response = 218; + ListRemoteDirectory list_remote_directory = 219; ListRemoteDirectoryResponse list_remote_directory_response = 220; UpdateDevServerProject update_dev_server_project = 221; - AddWorktree add_worktree = 222; AddWorktreeResponse add_worktree_response = 223; GetLlmToken get_llm_token = 235; GetLlmTokenResponse get_llm_token_response = 236; + + LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241; + LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242; // current max } reserved 158 to 161; @@ -2076,6 +2079,15 @@ message LspExtExpandMacroResponse { string expansion = 2; } +message LspExtSwitchSourceHeader { + uint64 project_id = 1; + uint64 buffer_id = 2; +} + +message LspExtSwitchSourceHeaderResponse { + string target_file = 1; +} + message SetRoomParticipantRole { uint64 room_id = 1; uint64 user_id = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 7ca48b1369..402c134c4e 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -406,6 +406,8 @@ messages!( (UpdateContext, Foreground), (SynchronizeContexts, Foreground), (SynchronizeContextsResponse, Foreground), + (LspExtSwitchSourceHeader, Background), + (LspExtSwitchSourceHeaderResponse, Background), (AddWorktree, Foreground), (AddWorktreeResponse, Foreground), ); @@ -528,6 +530,7 @@ request_messages!( (OpenContext, OpenContextResponse), (CreateContext, CreateContextResponse), (SynchronizeContexts, SynchronizeContextsResponse), + (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse), (AddWorktree, AddWorktreeResponse), ); @@ -597,6 +600,7 @@ entity_messages!( CreateContext, UpdateContext, SynchronizeContexts, + LspExtSwitchSourceHeader ); entity_messages!(