From b61171f1526f00c9916a5429af3d95b33d560d56 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 15 Mar 2025 09:50:32 +0200 Subject: [PATCH] Use `textDocument/codeLens` data in the actions menu when applicable (#26811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Similar to how tasks are fetched via LSP, also queries for document's code lens and filters the ones with the commands, supported in server capabilities. Whatever's left and applicable to the range given, is added to the actions menu: ![image](https://github.com/user-attachments/assets/6161e87f-f4b4-4173-8bf9-30db5e94b1ce) This way, Zed can get more actions to run, albeit neither r-a nor vtsls seem to provide anything by default. Currently, there are no plans to render code lens the way as in VSCode, it's just the extra actions that are show in the menu. ------------------ As part of the attempts to use rust-analyzer LSP data about the runnables, I've explored a way to get this data via standard LSP. When particular experimental client capabilities are enabled (similar to how clangd does this now), r-a starts to send back code lens with the data needed to run a cargo command: ``` {"jsonrpc":"2.0","id":48,"result":{"range":{"start":{"line":0,"character":0},"end":{"line":98,"character":0}},"command":{"title":"▶︎ Run Tests","command":"rust-analyzer.runSingle","arguments":[{"label":"test-mod tests::ecparser","location":{"targetUri":"file:///Users/someonetoignore/work/ec4rs/src/tests/ecparser.rs","targetRange":{"start":{"line":0,"character":0},"end":{"line":98,"character":0}},"targetSelectionRange":{"start":{"line":0,"character":0},"end":{"line":98,"character":0}}},"kind":"cargo","args":{"environment":{"RUSTC_TOOLCHAIN":"/Users/someonetoignore/.rustup/toolchains/1.85-aarch64-apple-darwin"},"cwd":"/Users/someonetoignore/work/ec4rs","overrideCargo":null,"workspaceRoot":"/Users/someonetoignore/work/ec4rs","cargoArgs":["test","--package","ec4rs","--lib"],"executableArgs":["tests::ecparser","--show-output"]}}]}}} ``` This data is passed as is to VSCode task processor, registered in https://github.com/rust-lang/rust-analyzer/blob/60cd01864a2d0d6e2231becee402ba063b59dfa1/editors/code/src/main.ts#L195 where it gets eventually executed as a VSCode's task, all handled by the r-a's extension code. rust-analyzer does not declare server capabilities for such tasks, and has no `workspace/executeCommand` handle, and Zed needs an interactive terminal output during the test runs, so we cannot ask rust-analyzer more than these descriptions. Given that Zed needs experimental capabilities set to get these lens: https://github.com/rust-lang/rust-analyzer/blob/60cd01864a2d0d6e2231becee402ba063b59dfa1/editors/code/src/client.ts#L318-L327 and that the lens may contain other odd tasks (e.g. docs opening or references lookup), a protocol extension to get runnables looks more preferred than lens: https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables This PR does not include any work on this direction, limiting to the general code lens support. As a proof of concept, it's possible to get the lens and even attempt to run it, to no avail: ![image](https://github.com/user-attachments/assets/56950880-d387-48f9-b865-727f97b5633b) Release Notes: - Used `textDocument/codeLens` data in the actions menu when applicable --- crates/assistant/src/inline_assistant.rs | 1 + crates/assistant2/src/inline_assistant.rs | 1 + crates/collab/src/rpc.rs | 2 + crates/editor/src/editor.rs | 32 +++- crates/editor/src/editor_tests.rs | 181 ++++++++++++++++++ crates/languages/src/rust.rs | 2 +- crates/lsp/src/lsp.rs | 6 + crates/project/src/lsp_command.rs | 166 +++++++++++++++- crates/project/src/lsp_store.rs | 159 ++++++++++++++- .../src/lsp_store/rust_analyzer_ext.rs | 8 + crates/project/src/project.rs | 42 ++++ crates/proto/proto/zed.proto | 30 ++- crates/proto/src/proto.rs | 7 + 13 files changed, 618 insertions(+), 19 deletions(-) diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index fb7343c18b0e5f422ec55d163b365d2f3af13c39..79cf97ff8860afbb6371219a12a6fe96a7f6bb7d 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -3569,6 +3569,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { title: "Fix with Assistant".into(), ..Default::default() })), + resolved: true, }])) } else { Task::ready(Ok(Vec::new())) diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index e76834da73dd0549994187d6cf6d3745c9083fad..fd42ed1a24c99c00c1596e1c2417ee2d72c8fa46 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -1729,6 +1729,7 @@ impl CodeActionProvider for AssistantCodeActionProvider { title: "Fix with Assistant".into(), ..Default::default() })), + resolved: true, }])) } else { Task::ready(Ok(Vec::new())) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index c820fcdfc8bc8feec7af99c8472ea7c42d3ffd5b..94c82327d0fafc4de99774f12aa473f2ce9daa05 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -307,6 +307,7 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) @@ -347,6 +348,7 @@ impl Server { .add_message_handler(create_buffer_for_peer) .add_request_handler(update_buffer) .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 987d9e29eb279e0bad42e41367d9154bbe8fdda7..0c863cfab89d4735682012e2efdd5e5c5fb6ac93 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -69,7 +69,7 @@ pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, }; use futures::{ - future::{self, Shared}, + future::{self, join, Shared}, FutureExt, }; use fuzzy::StringMatchCandidate; @@ -82,10 +82,10 @@ use code_context_menus::{ use git::blame::GitBlame; use gpui::{ div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation, - AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds, - ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, - EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, - HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, + AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, AvailableSpace, Background, + Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, + EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, + Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, @@ -1233,11 +1233,15 @@ impl Editor { project_subscriptions.push(cx.subscribe_in( project, window, - |editor, _, event, window, cx| { - if let project::Event::RefreshInlayHints = event { + |editor, _, event, window, cx| match event { + project::Event::RefreshCodeLens => { + // we always query lens with actions, without storing them, always refreshing them + } + project::Event::RefreshInlayHints => { editor .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } else if let project::Event::SnippetEdit(id, snippet_edits) = event { + } + project::Event::SnippetEdit(id, snippet_edits) => { if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { let focus_handle = editor.focus_handle(cx); if focus_handle.is_focused(window) { @@ -1257,6 +1261,7 @@ impl Editor { } } } + _ => {} }, )); if let Some(task_inventory) = project @@ -17027,7 +17032,16 @@ impl CodeActionProvider for Entity { cx: &mut App, ) -> Task>> { self.update(cx, |project, cx| { - project.code_actions(buffer, range, None, cx) + let code_lens = project.code_lens(buffer, range.clone(), cx); + let code_actions = project.code_actions(buffer, range, None, cx); + cx.background_spawn(async move { + let (code_lens, code_actions) = join(code_lens, code_actions).await; + Ok(code_lens + .context("code lens fetch")? + .into_iter() + .chain(code_actions.context("code action fetch")?) + .collect()) + }) }) } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a37e0efc3b389ea8c149db13358f34931cb54bff..3482aefec3b24f1de7dad94b28724622c9ee01ce 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -17233,6 +17233,187 @@ async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) { "}); } +#[gpui::test(iterations = 10)] +async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "a.ts": "a", + }), + ) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(Arc::new(Language::new( + LanguageConfig { + name: "TypeScript".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), + ))); + let mut fake_language_servers = language_registry.register_fake_lsp( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + code_lens_provider: Some(lsp::CodeLensOptions { + resolve_provider: Some(true), + }), + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec!["_the/command".to_string()], + ..lsp::ExecuteCommandOptions::default() + }), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); + + let (buffer, _handle) = project + .update(cx, |p, cx| { + p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left); + drop(buffer_snapshot); + let actions = cx + .update_window(*workspace, |_, window, cx| { + project.code_actions(&buffer, anchor..anchor, window, cx) + }) + .unwrap(); + + fake_server + .handle_request::(|_, _| async move { + Ok(Some(vec![ + lsp::CodeLens { + range: lsp::Range::default(), + command: Some(lsp::Command { + title: "Code lens command".to_owned(), + command: "_the/command".to_owned(), + arguments: None, + }), + data: None, + }, + lsp::CodeLens { + range: lsp::Range::default(), + command: Some(lsp::Command { + title: "Command not in capabilities".to_owned(), + command: "not in capabilities".to_owned(), + arguments: None, + }), + data: None, + }, + lsp::CodeLens { + range: lsp::Range { + start: lsp::Position { + line: 1, + character: 1, + }, + end: lsp::Position { + line: 1, + character: 1, + }, + }, + command: Some(lsp::Command { + title: "Command not in range".to_owned(), + command: "_the/command".to_owned(), + arguments: None, + }), + data: None, + }, + ])) + }) + .next() + .await; + + let actions = actions.await.unwrap(); + assert_eq!( + actions.len(), + 1, + "Should have only one valid action for the 0..0 range" + ); + let action = actions[0].clone(); + let apply = project.update(cx, |project, cx| { + project.apply_code_action(buffer.clone(), action, true, cx) + }); + + // Resolving the code action does not populate its edits. In absence of + // edits, we must execute the given command. + fake_server.handle_request::(|mut lens, _| async move { + let lens_command = lens.command.as_mut().expect("should have a command"); + assert_eq!(lens_command.title, "Code lens command"); + lens_command.arguments = Some(vec![json!("the-argument")]); + Ok(lens) + }); + + // While executing the command, the language server sends the editor + // a `workspaceEdit` request. + fake_server + .handle_request::({ + let fake = fake_server.clone(); + move |params, _| { + assert_eq!(params.command, "_the/command"); + let fake = fake.clone(); + async move { + fake.server + .request::( + lsp::ApplyWorkspaceEditParams { + label: None, + edit: lsp::WorkspaceEdit { + changes: Some( + [( + lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(), + vec![lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "X".into(), + }], + )] + .into_iter() + .collect(), + ), + ..Default::default() + }, + }, + ) + .await + .unwrap(); + Ok(Some(json!(null))) + } + } + }) + .next() + .await; + + // Applying the code lens command returns a project transaction containing the edits + // sent by the language server in its `workspaceEdit` request. + let transaction = apply.await.unwrap(); + assert!(transaction.0.contains_key(&buffer)); + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "Xa"); + buffer.undo(cx); + assert_eq!(buffer.text(), "a"); + }); +} + mod autoclose_tags { use super::*; use language::language_settings::JsxTagAutoCloseSettings; diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index f70bc56d1b65e3f2c20339ee11449cc36ffeb5be..6602cd8bad862eb32cc91eeb66a43b094fef962e 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -7,7 +7,7 @@ use gpui::{App, AsyncApp, Task}; use http_client::github::AssetKind; use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; pub use language::*; -use lsp::{LanguageServerBinary, LanguageServerName}; +use lsp::LanguageServerBinary; use regex::Regex; use smol::fs::{self}; use std::fmt::Display; diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 132a67b5b7d4017d84ba863a9009f66e7554780f..de7e5871645a51a2e23bd7336ebc171ec83571e6 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -632,6 +632,9 @@ impl LanguageServer { diagnostic: Some(DiagnosticWorkspaceClientCapabilities { refresh_support: None, }), + code_lens: Some(CodeLensWorkspaceClientCapabilities { + refresh_support: Some(true), + }), workspace_edit: Some(WorkspaceEditClientCapabilities { resource_operations: Some(vec![ ResourceOperationKind::Create, @@ -763,6 +766,9 @@ impl LanguageServer { did_save: Some(true), ..TextDocumentSyncClientCapabilities::default() }), + code_lens: Some(CodeLensClientCapabilities { + dynamic_registration: Some(false), + }), ..TextDocumentClientCapabilities::default() }), experimental: Some(json!({ diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 5eb16bc74c37bb744b8342699be7a4c68ad3204f..fac968be7869d94ced69031536645496ea90810f 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -234,6 +234,19 @@ pub(crate) struct InlayHints { pub range: Range, } +#[derive(Debug, Copy, Clone)] +pub(crate) struct GetCodeLens; + +impl GetCodeLens { + pub(crate) fn can_resolve_lens(capabilities: &ServerCapabilities) -> bool { + capabilities + .code_lens_provider + .as_ref() + .and_then(|code_lens_options| code_lens_options.resolve_provider) + .unwrap_or(false) + } +} + #[derive(Debug)] pub(crate) struct LinkedEditingRange { pub position: Anchor, @@ -2229,18 +2242,18 @@ impl LspCommand for GetCodeActions { .unwrap_or_default() .into_iter() .filter_map(|entry| { - let lsp_action = match entry { + let (lsp_action, resolved) = match entry { lsp::CodeActionOrCommand::CodeAction(lsp_action) => { if let Some(command) = lsp_action.command.as_ref() { if !available_commands.contains(&command.command) { return None; } } - LspAction::Action(Box::new(lsp_action)) + (LspAction::Action(Box::new(lsp_action)), false) } lsp::CodeActionOrCommand::Command(command) => { if available_commands.contains(&command.command) { - LspAction::Command(command) + (LspAction::Command(command), true) } else { return None; } @@ -2259,6 +2272,7 @@ impl LspCommand for GetCodeActions { server_id, range: self.range.clone(), lsp_action, + resolved, }) }) .collect()) @@ -3037,6 +3051,152 @@ impl LspCommand for InlayHints { } } +#[async_trait(?Send)] +impl LspCommand for GetCodeLens { + type Response = Vec; + type LspRequest = lsp::CodeLensRequest; + type ProtoRequest = proto::GetCodeLens; + + fn display_name(&self) -> &str { + "Code Lens" + } + + fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { + capabilities + .server_capabilities + .code_lens_provider + .as_ref() + .map_or(false, |code_lens_options| { + code_lens_options.resolve_provider.unwrap_or(false) + }) + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + _: &Arc, + _: &App, + ) -> Result { + Ok(lsp::CodeLensParams { + text_document: lsp::TextDocumentIdentifier { + uri: file_path_to_lsp_url(path)?, + }, + work_done_progress_params: lsp::WorkDoneProgressParams::default(), + partial_result_params: lsp::PartialResultParams::default(), + }) + } + + async fn response_from_lsp( + self, + message: Option>, + lsp_store: Entity, + buffer: Entity, + server_id: LanguageServerId, + mut cx: AsyncApp, + ) -> anyhow::Result> { + let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; + let language_server = cx.update(|cx| { + lsp_store + .read(cx) + .language_server_for_id(server_id) + .with_context(|| { + format!("Missing the language server that just returned a response {server_id}") + }) + })??; + let server_capabilities = language_server.capabilities(); + let available_commands = server_capabilities + .execute_command_provider + .as_ref() + .map(|options| options.commands.as_slice()) + .unwrap_or_default(); + Ok(message + .unwrap_or_default() + .into_iter() + .filter(|code_lens| { + code_lens + .command + .as_ref() + .is_none_or(|command| available_commands.contains(&command.command)) + }) + .map(|code_lens| { + let code_lens_range = range_from_lsp(code_lens.range); + let start = snapshot.clip_point_utf16(code_lens_range.start, Bias::Left); + let end = snapshot.clip_point_utf16(code_lens_range.end, Bias::Right); + let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); + CodeAction { + server_id, + range, + lsp_action: LspAction::CodeLens(code_lens), + resolved: false, + } + }) + .collect()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeLens { + proto::GetCodeLens { + project_id, + buffer_id: buffer.remote_id().into(), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetCodeLens, + _: Entity, + buffer: Entity, + mut cx: AsyncApp, + ) -> Result { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + })? + .await?; + Ok(Self) + } + + fn response_to_proto( + response: Vec, + _: &mut LspStore, + _: PeerId, + buffer_version: &clock::Global, + _: &mut App, + ) -> proto::GetCodeLensResponse { + proto::GetCodeLensResponse { + lens_actions: response + .iter() + .map(LspStore::serialize_code_action) + .collect(), + version: serialize_version(buffer_version), + } + } + + async fn response_from_proto( + self, + message: proto::GetCodeLensResponse, + _: Entity, + buffer: Entity, + mut cx: AsyncApp, + ) -> anyhow::Result> { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + })? + .await?; + message + .lens_actions + .into_iter() + .map(LspStore::deserialize_code_action) + .collect::>>() + .context("deserializing proto code lens response") + } + + fn buffer_id_from_proto(message: &proto::GetCodeLens) -> Result { + BufferId::new(message.buffer_id) + } +} + #[async_trait(?Send)] impl LspCommand for LinkedEditingRange { type Response = Vec>; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 81afa08813a19a054d413df8d1d7b66d91888226..4300f65be017ed508685e8fee4330734cd8c44e3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -807,6 +807,27 @@ impl LocalLspStore { }) .detach(); + language_server + .on_request::({ + let this = this.clone(); + move |(), mut cx| { + let this = this.clone(); + async move { + this.update(&mut cx, |this, cx| { + cx.emit(LspStoreEvent::RefreshCodeLens); + this.downstream_client.as_ref().map(|(client, project_id)| { + client.send(proto::RefreshCodeLens { + project_id: *project_id, + }) + }) + })? + .transpose()?; + Ok(()) + } + } + }) + .detach(); + language_server .on_request::({ let this = this.clone(); @@ -1628,9 +1649,8 @@ impl LocalLspStore { ) -> anyhow::Result<()> { match &mut action.lsp_action { LspAction::Action(lsp_action) => { - if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) - && lsp_action.data.is_some() - && (lsp_action.command.is_none() || lsp_action.edit.is_none()) + if !action.resolved + && GetCodeActions::can_resolve_actions(&lang_server.capabilities()) { *lsp_action = Box::new( lang_server @@ -1639,8 +1659,17 @@ impl LocalLspStore { ); } } + LspAction::CodeLens(lens) => { + if !action.resolved && GetCodeLens::can_resolve_lens(&lang_server.capabilities()) { + *lens = lang_server + .request::(lens.clone()) + .await?; + } + } LspAction::Command(_) => {} } + + action.resolved = true; anyhow::Ok(()) } @@ -2887,6 +2916,7 @@ pub enum LspStoreEvent { }, Notification(String), RefreshInlayHints, + RefreshCodeLens, DiagnosticsUpdated { language_server_id: LanguageServerId, path: ProjectPath, @@ -2942,6 +2972,7 @@ impl LspStore { client.add_entity_request_handler(Self::handle_resolve_inlay_hint); client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); client.add_entity_request_handler(Self::handle_refresh_inlay_hints); + client.add_entity_request_handler(Self::handle_refresh_code_lens); client.add_entity_request_handler(Self::handle_on_type_formatting); client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers); @@ -4316,6 +4347,7 @@ impl LspStore { cx, ) } + pub fn code_actions( &mut self, buffer_handle: &Entity, @@ -4395,6 +4427,66 @@ impl LspStore { } } + pub fn code_lens( + &mut self, + buffer_handle: &Entity, + cx: &mut Context, + ) -> Task>> { + if let Some((upstream_client, project_id)) = self.upstream_client() { + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), + project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetCodeLens( + GetCodeLens.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); + cx.spawn(|weak_project, cx| async move { + let Some(project) = weak_project.upgrade() else { + return Ok(Vec::new()); + }; + let responses = request_task.await?.responses; + let code_lens = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetCodeLensResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|code_lens_response| { + GetCodeLens.response_from_proto( + code_lens_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) + .await; + + Ok(code_lens + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .collect()) + }) + } else { + let code_lens_task = + self.request_multiple_lsp_locally(buffer_handle, None::, GetCodeLens, cx); + cx.spawn(|_, _| async move { Ok(code_lens_task.await.into_iter().flatten().collect()) }) + } + } + #[inline(never)] pub fn completions( &self, @@ -6308,6 +6400,43 @@ impl LspStore { .collect(), }) } + Some(proto::multi_lsp_query::Request::GetCodeLens(get_code_lens)) => { + let get_code_lens = GetCodeLens::from_proto( + get_code_lens, + this.clone(), + buffer.clone(), + cx.clone(), + ) + .await?; + + let code_lens_actions = this + .update(&mut cx, |project, cx| { + project.request_multiple_lsp_locally( + &buffer, + None::, + get_code_lens, + cx, + ) + })? + .await + .into_iter(); + + this.update(&mut cx, |project, cx| proto::MultiLspQueryResponse { + responses: code_lens_actions + .map(|actions| proto::LspResponse { + response: Some(proto::lsp_response::Response::GetCodeLensResponse( + GetCodeLens::response_to_proto( + actions, + project, + sender_id, + &buffer_version, + cx, + ), + )), + }) + .collect(), + }) + } None => anyhow::bail!("empty multi lsp query request"), } } @@ -7211,6 +7340,17 @@ impl LspStore { }) } + async fn handle_refresh_code_lens( + this: Entity, + _: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + this.update(&mut cx, |_, cx| { + cx.emit(LspStoreEvent::RefreshCodeLens); + })?; + Ok(proto::Ack {}) + } + async fn handle_open_buffer_for_symbol( this: Entity, envelope: TypedEnvelope, @@ -8434,6 +8574,10 @@ impl LspStore { proto::code_action::Kind::Command as i32, serde_json::to_vec(command).unwrap(), ), + LspAction::CodeLens(code_lens) => ( + proto::code_action::Kind::CodeLens as i32, + serde_json::to_vec(code_lens).unwrap(), + ), }; proto::CodeAction { @@ -8442,6 +8586,7 @@ impl LspStore { end: Some(serialize_anchor(&action.range.end)), lsp_action, kind, + resolved: action.resolved, } } @@ -8449,11 +8594,11 @@ impl LspStore { let start = action .start .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid start"))?; + .context("invalid start")?; let end = action .end .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid end"))?; + .context("invalid end")?; let lsp_action = match proto::code_action::Kind::from_i32(action.kind) { Some(proto::code_action::Kind::Action) => { LspAction::Action(serde_json::from_slice(&action.lsp_action)?) @@ -8461,11 +8606,15 @@ impl LspStore { Some(proto::code_action::Kind::Command) => { LspAction::Command(serde_json::from_slice(&action.lsp_action)?) } + Some(proto::code_action::Kind::CodeLens) => { + LspAction::CodeLens(serde_json::from_slice(&action.lsp_action)?) + } None => anyhow::bail!("Unknown action kind {}", action.kind), }; Ok(CodeAction { server_id: LanguageServerId(action.server_id as usize), range: start..end, + resolved: action.resolved, lsp_action, }) } diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index f69a213fa143a6968dc967d3f4e26707e19ab6e1..d049ac2c4e4d54d086b7ef39e571b0566c16413a 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -6,6 +6,14 @@ use crate::{LanguageServerPromptRequest, LspStore, LspStoreEvent}; pub const RUST_ANALYZER_NAME: &str = "rust-analyzer"; +pub const EXTRA_SUPPORTED_COMMANDS: &[&str] = &[ + "rust-analyzer.runSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "rust-analyzer.triggerParameterHints", + "rust-analyzer.rename", +]; + /// Experimental: Informs the end user about the state of the server /// /// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6593b7875d7c791404aace13e8d4e22ee2d42922..d29518b2817a0e42ed3e065ffc70b4f4117c80a8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -280,6 +280,7 @@ pub enum Event { Reshared, Rejoined, RefreshInlayHints, + RefreshCodeLens, RevealInProjectPanel(ProjectEntryId), SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), ExpandedAllForEntry(WorktreeId, ProjectEntryId), @@ -509,6 +510,8 @@ pub struct CodeAction { /// The raw code action provided by the language server. /// Can be either an action or a command. pub lsp_action: LspAction, + /// Whether the action needs to be resolved using the language server. + pub resolved: bool, } /// An action sent back by a language server. @@ -519,6 +522,8 @@ pub enum LspAction { Action(Box), /// A command data to run as an action. Command(lsp::Command), + /// A code lens data to run as an action. + CodeLens(lsp::CodeLens), } impl LspAction { @@ -526,6 +531,11 @@ impl LspAction { match self { Self::Action(action) => &action.title, Self::Command(command) => &command.title, + Self::CodeLens(lens) => lens + .command + .as_ref() + .map(|command| command.title.as_str()) + .unwrap_or("Unknown command"), } } @@ -533,6 +543,7 @@ impl LspAction { match self { Self::Action(action) => action.kind.clone(), Self::Command(_) => Some(lsp::CodeActionKind::new("command")), + Self::CodeLens(_) => Some(lsp::CodeActionKind::new("code lens")), } } @@ -540,6 +551,7 @@ impl LspAction { match self { Self::Action(action) => action.edit.as_ref(), Self::Command(_) => None, + Self::CodeLens(_) => None, } } @@ -547,6 +559,7 @@ impl LspAction { match self { Self::Action(action) => action.command.as_ref(), Self::Command(command) => Some(command), + Self::CodeLens(lens) => lens.command.as_ref(), } } } @@ -2483,6 +2496,7 @@ impl Project { }; } LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints), + LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens), LspStoreEvent::LanguageServerPrompt(prompt) => { cx.emit(Event::LanguageServerPrompt(prompt.clone())) } @@ -3163,6 +3177,34 @@ impl Project { }) } + pub fn code_lens( + &mut self, + buffer_handle: &Entity, + range: Range, + cx: &mut Context, + ) -> Task>> { + let snapshot = buffer_handle.read(cx).snapshot(); + let range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end); + let code_lens_actions = self + .lsp_store + .update(cx, |lsp_store, cx| lsp_store.code_lens(buffer_handle, cx)); + + cx.background_spawn(async move { + let mut code_lens_actions = code_lens_actions.await?; + code_lens_actions.retain(|code_lens_action| { + range + .start + .cmp(&code_lens_action.range.start, &snapshot) + .is_ge() + && range + .end + .cmp(&code_lens_action.range.end, &snapshot) + .is_le() + }); + Ok(code_lens_actions) + }) + } + pub fn apply_code_action( &self, buffer_handle: Entity, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index c8a1507a9a457e723e56f0dc3792e6367ba9ed93..f4b8366cd076994fb889af7cdc8740a63c6eba57 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -346,7 +346,12 @@ message Envelope { GitDiff git_diff = 319; GitDiffResponse git_diff_response = 320; - GitInit git_init = 321; // current max + GitInit git_init = 321; + + CodeLens code_lens = 322; + GetCodeLens get_code_lens = 323; + GetCodeLensResponse get_code_lens_response = 324; + RefreshCodeLens refresh_code_lens = 325; // current max } reserved 87 to 88; @@ -1263,6 +1268,25 @@ message RefreshInlayHints { uint64 project_id = 1; } +message CodeLens { + bytes lsp_lens = 1; +} + +message GetCodeLens { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; +} + +message GetCodeLensResponse { + repeated CodeAction lens_actions = 1; + repeated VectorClockEntry version = 2; +} + +message RefreshCodeLens { + uint64 project_id = 1; +} + message MarkupContent { bool is_markdown = 1; string value = 2; @@ -1298,9 +1322,11 @@ message CodeAction { Anchor end = 3; bytes lsp_action = 4; Kind kind = 5; + bool resolved = 6; enum Kind { Action = 0; Command = 1; + CodeLens = 2; } } @@ -2346,6 +2372,7 @@ message MultiLspQuery { GetHover get_hover = 5; GetCodeActions get_code_actions = 6; GetSignatureHelp get_signature_help = 7; + GetCodeLens get_code_lens = 8; } } @@ -2365,6 +2392,7 @@ message LspResponse { GetHoverResponse get_hover_response = 1; GetCodeActionsResponse get_code_actions_response = 2; GetSignatureHelpResponse get_signature_help_response = 3; + GetCodeLensResponse get_code_lens_response = 4; } } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 16a08cd42f73094920a20f8d0ed484233f2c88f6..53e1a8f653b771593cea53ecad032fd413454096 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -340,6 +340,9 @@ messages!( (ResolveCompletionDocumentationResponse, Background), (ResolveInlayHint, Background), (ResolveInlayHintResponse, Background), + (RefreshCodeLens, Background), + (GetCodeLens, Background), + (GetCodeLensResponse, Background), (RespondToChannelInvite, Foreground), (RespondToContactRequest, Foreground), (RoomUpdated, Foreground), @@ -513,6 +516,7 @@ request_messages!( (GetUsers, UsersResponse), (IncomingCall, Ack), (InlayHints, InlayHintsResponse), + (GetCodeLens, GetCodeLensResponse), (InviteChannelMember, Ack), (JoinChannel, JoinRoomResponse), (JoinChannelBuffer, JoinChannelBufferResponse), @@ -534,6 +538,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (CountLanguageModelTokens, CountLanguageModelTokensResponse), (RefreshInlayHints, Ack), + (RefreshCodeLens, Ack), (RejoinChannelBuffers, RejoinChannelBuffersResponse), (RejoinRoom, RejoinRoomResponse), (ReloadBuffers, ReloadBuffersResponse), @@ -632,6 +637,7 @@ entity_messages!( ApplyCodeActionKind, FormatBuffers, GetCodeActions, + GetCodeLens, GetCompletions, GetDefinition, GetDeclaration, @@ -659,6 +665,7 @@ entity_messages!( PerformRename, PrepareRename, RefreshInlayHints, + RefreshCodeLens, ReloadBuffers, RemoveProjectCollaborator, RenameProjectEntry,