Use textDocument/codeLens data in the actions menu when applicable (#26811)

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


60cd01864a/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:

60cd01864a/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
This commit is contained in:
Kirill Bulatov 2025-03-15 09:50:32 +02:00 committed by GitHub
parent 0b492c11de
commit b61171f152
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 618 additions and 19 deletions

View file

@ -807,6 +807,27 @@ impl LocalLspStore {
})
.detach();
language_server
.on_request::<lsp::request::CodeLensRefresh, _, _>({
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::<lsp::request::ShowMessageRequest, _, _>({
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::<lsp::request::CodeLensResolve>(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<Buffer>,
@ -4395,6 +4427,66 @@ impl LspStore {
}
}
pub fn code_lens(
&mut self,
buffer_handle: &Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<Vec<CodeAction>>> {
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::<Result<Vec<Vec<_>>>>()?
.into_iter()
.flatten()
.collect())
})
} else {
let code_lens_task =
self.request_multiple_lsp_locally(buffer_handle, None::<usize>, 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::<usize>,
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<Self>,
_: TypedEnvelope<proto::RefreshCodeLens>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
this.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::RefreshCodeLens);
})?;
Ok(proto::Ack {})
}
async fn handle_open_buffer_for_symbol(
this: Entity<Self>,
envelope: TypedEnvelope<proto::OpenBufferForSymbol>,
@ -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,
})
}