From 703c9655a026f38bb62d3359dde062bf91a99638 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 5 Mar 2024 23:42:12 +0200 Subject: [PATCH] Always resolve code action if needed (#8904) Follow-up of https://github.com/zed-industries/zed/pull/8874 and https://github.com/zed-industries/zed/pull/7635 Closes https://github.com/zed-industries/zed/issues/7609 * mentions all `lsp::CodeActions` properties in the Zed client resolve capabilities to remove more json out of general actions request potentially * removes odd `CodeActions.data` field checks, as that field is opaque and is intended to store data, needed by the langserver to resolve this code action * if any `CodeAction` lacks either `command` or `edits` fields, tries to resolve the action This all effectively causes Zed to always fire an action resolve request, since we update actions list (replacing the resolved actions with the new, unresolved ones) via `refresh_code_actions` https://github.com/zed-industries/zed/blob/9e66d48ccd3bb87ee97aec4e63484dc8b78e45ac/crates/editor/src/editor.rs#L3650 that is being called on selections change and the actions menu open. Yet, we do not query the resolve until the action is either applied (selected in the list), or called for formatting, so it seems to be fine to resolve them always, as it's not a frequent operation such as reacting to every keystroke. Release Notes: - Fixed certain code actions not being resolved properly ([7609](https://github.com/zed-industries/zed/issues/7609)) --------- Co-authored-by: Derrick Laird --- crates/lsp/src/lsp.rs | 9 ++++- crates/project/src/lsp_command.rs | 13 ++++++ crates/project/src/project.rs | 62 ++++++++++++++--------------- crates/project/src/project_tests.rs | 40 ++++++++++++++----- 4 files changed, 81 insertions(+), 43 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 1652e340445ab711193ce7268a5921597082de05..b1099cda5f9a7b110ea9c9080cf9dc43eb338ce5 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -570,7 +570,14 @@ impl LanguageServer { }), data_support: Some(true), resolve_support: Some(CodeActionCapabilityResolveSupport { - properties: vec!["edit".to_string(), "command".to_string()], + properties: vec![ + "kind".to_string(), + "diagnostics".to_string(), + "isPreferred".to_string(), + "disabled".to_string(), + "edit".to_string(), + "command".to_string(), + ], }), ..Default::default() }), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 2f8e409afa78ac1a6fa3b458ed5f548bfc95657b..8cbd83a50ca1d26b5d31a367ec052af601153f92 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1834,6 +1834,19 @@ impl LspCommand for GetCodeActions { } } +impl GetCodeActions { + pub fn can_resolve_actions(capabilities: &ServerCapabilities) -> bool { + capabilities + .code_action_provider + .as_ref() + .and_then(|options| match options { + lsp::CodeActionProviderCapability::Simple(_is_supported) => None, + lsp::CodeActionProviderCapability::Options(options) => options.resolve_provider, + }) + .unwrap_or(false) + } +} + #[async_trait(?Send)] impl LspCommand for OnTypeFormatting { type Response = Option; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 519764eb616bb5ee1c03226763754a74551007ea..a82b156b03d16d1d1ba1387bed5d429fc5256743 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -40,11 +40,11 @@ use language::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, - range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, - CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, - Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, - LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, - PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeAction, + CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, + Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, + LspAdapterDelegate, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -4360,7 +4360,10 @@ impl Project { })? .await?; - for action in actions { + for mut action in actions { + Self::try_resolve_code_action(&language_server, &mut action) + .await + .context("resolving a formatting code action")?; if let Some(edit) = action.lsp_action.edit { if edit.changes.is_none() && edit.document_changes.is_none() { continue; @@ -4380,6 +4383,7 @@ impl Project { project_transaction.0.extend(new.0); } + // TODO kb here too: if let Some(command) = action.lsp_action.command { project.update(&mut cx, |this, _| { this.last_workspace_edits_by_language_server @@ -5422,33 +5426,10 @@ impl Project { } else { return Task::ready(Ok(Default::default())); }; - let range = action.range.to_point_utf16(buffer); - cx.spawn(move |this, mut cx| async move { - if let Some(lsp_range) = action - .lsp_action - .data - .as_mut() - .and_then(|d| d.get_mut("codeActionParams")) - .and_then(|d| d.get_mut("range")) - { - *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap(); - action.lsp_action = lang_server - .request::(action.lsp_action) - .await?; - } else { - let actions = this - .update(&mut cx, |this, cx| { - this.code_actions(&buffer_handle, action.range, cx) - })? - .await?; - action.lsp_action = actions - .into_iter() - .find(|a| a.lsp_action.title == action.lsp_action.title) - .ok_or_else(|| anyhow!("code action is outdated"))? - .lsp_action; - } - + Self::try_resolve_code_action(&lang_server, &mut action) + .await + .context("resolving a code action")?; if let Some(edit) = action.lsp_action.edit { if edit.changes.is_some() || edit.document_changes.is_some() { return Self::deserialize_workspace_edit( @@ -8322,6 +8303,23 @@ impl Project { }) } + async fn try_resolve_code_action( + lang_server: &LanguageServer, + action: &mut CodeAction, + ) -> anyhow::Result<()> { + if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) { + if action.lsp_action.data.is_some() + && (action.lsp_action.command.is_none() || action.lsp_action.edit.is_none()) + { + action.lsp_action = lang_server + .request::(action.lsp_action.clone()) + .await?; + } + } + + anyhow::Ok(()) + } + async fn handle_refresh_inlay_hints( this: Model, _: TypedEnvelope, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 1af8fca291fbaa277cf1dc118b7f4c0ba08b7c36..c513b9a2c4803022ef594010a081d677ceb17efa 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2475,8 +2475,21 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(typescript_lang()); - let mut fake_language_servers = - language_registry.register_fake_lsp_adapter("TypeScript", Default::default()); + let mut fake_language_servers = language_registry.register_fake_lsp_adapter( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + code_action_provider: Some(lsp::CodeActionProviderCapability::Options( + lsp::CodeActionOptions { + resolve_provider: Some(true), + ..lsp::CodeActionOptions::default() + }, + )), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); let buffer = project .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) @@ -2492,16 +2505,14 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { Ok(Some(vec![ lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { title: "The code action".into(), - command: Some(lsp::Command { - title: "The command".into(), - command: "_the/command".into(), - arguments: Some(vec![json!("the-argument")]), - }), - ..Default::default() + data: Some(serde_json::json!({ + "command": "_the/command", + })), + ..lsp::CodeAction::default() }), lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { title: "two".into(), - ..Default::default() + ..lsp::CodeAction::default() }), ])) }) @@ -2516,7 +2527,16 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { // Resolving the code action does not populate its edits. In absence of // edits, we must execute the given command. fake_server.handle_request::( - |action, _| async move { Ok(action) }, + |mut action, _| async move { + if action.data.is_some() { + action.command = Some(lsp::Command { + title: "The command".into(), + command: "_the/command".into(), + arguments: Some(vec![json!("the-argument")]), + }); + } + Ok(action) + }, ); // While executing the command, the language server sends the editor