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

@ -234,6 +234,19 @@ pub(crate) struct InlayHints {
pub range: Range<Anchor>,
}
#[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<CodeAction>;
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<LanguageServer>,
_: &App,
) -> Result<lsp::CodeLensParams> {
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<Vec<lsp::CodeLens>>,
lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncApp,
) -> anyhow::Result<Vec<CodeAction>> {
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<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Self> {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
})?
.await?;
Ok(Self)
}
fn response_to_proto(
response: Vec<CodeAction>,
_: &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<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> anyhow::Result<Vec<CodeAction>> {
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::<Result<Vec<_>>>()
.context("deserializing proto code lens response")
}
fn buffer_id_from_proto(message: &proto::GetCodeLens) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[async_trait(?Send)]
impl LspCommand for LinkedEditingRange {
type Response = Vec<Range<Anchor>>;

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

View file

@ -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)

View file

@ -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<lsp::CodeAction>),
/// 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<T: Clone + ToOffset>(
&mut self,
buffer_handle: &Entity<Buffer>,
range: Range<T>,
cx: &mut Context<Self>,
) -> Task<Result<Vec<CodeAction>>> {
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<Buffer>,