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

Re-applies what's been reverted in
https://github.com/zed-industries/zed/pull/26832 with an action-related
fix in
64b5d37d32

Before, actions were resolved only if `data` is present and either of
the possible fields is empty:

e842b4eade/crates/project/src/lsp_store.rs (L1632-L1633)

But Zed resolves completions and inlays once, unconditionally, and the
reverted PR applied the same strategy to actions.
That did not work despite the spec not forbidding `data`-less actions to
be resolved.

Soon, it starts to work due to
https://github.com/rust-lang/rust-analyzer/pull/19369 but it seems safer
to restore the original filtering code.

Code lens have no issues with `data`-less resolves:

220d913cbc/crates/rust-analyzer/src/handlers/request.rs (L1618-L1620)

so the same approach as completions and inlays is kept: resolve once.


Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-03-15 22:09:32 +02:00 committed by GitHub
parent ef91e7afae
commit 8a31dcaeb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 618 additions and 17 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>>;