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`

9e66d48ccd/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 <swampdonk@gmail.com>
This commit is contained in:
Kirill Bulatov 2024-03-05 23:42:12 +02:00 committed by GitHub
parent addfcdea8d
commit 703c9655a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 81 additions and 43 deletions

View file

@ -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()
}),

View file

@ -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<Transaction>;

View file

@ -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::<lsp::request::CodeActionResolveRequest>(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::<lsp::request::CodeActionResolveRequest>(action.lsp_action.clone())
.await?;
}
}
anyhow::Ok(())
}
async fn handle_refresh_inlay_hints(
this: Model<Self>,
_: TypedEnvelope<proto::RefreshInlayHints>,

View file

@ -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::<lsp::request::CodeActionResolveRequest, _, _>(
|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