diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 17ddd729cb..9e33895f4f 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -38,7 +38,7 @@ use language_model::{ use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; -use project::{CodeAction, ProjectTransaction}; +use project::{ActionVariant, CodeAction, ProjectTransaction}; use prompt_store::PromptBuilder; use rope::Rope; use settings::{update_settings_file, Settings, SettingsStore}; @@ -3569,10 +3569,10 @@ impl CodeActionProvider for AssistantCodeActionProvider { Task::ready(Ok(vec![CodeAction { server_id: language::LanguageServerId(0), range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end), - lsp_action: lsp::CodeAction { + lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction { title: "Fix with Assistant".into(), ..Default::default() - }, + })), }])) } else { Task::ready(Ok(Vec::new())) diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index 803039bf59..607f02344d 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/crates/assistant2/src/inline_assistant.rs @@ -27,6 +27,7 @@ use language::{Buffer, Point, Selection, TransactionId}; use language_model::{report_assistant_event, LanguageModelRegistry}; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; +use project::ActionVariant; use project::{CodeAction, ProjectTransaction}; use prompt_store::PromptBuilder; use settings::{Settings, SettingsStore}; @@ -1727,10 +1728,10 @@ impl CodeActionProvider for AssistantCodeActionProvider { Task::ready(Ok(vec![CodeAction { server_id: language::LanguageServerId(0), range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end), - lsp_action: lsp::CodeAction { + lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction { title: "Fix with Assistant".into(), ..Default::default() - }, + })), }])) } else { Task::ready(Ok(Vec::new())) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 4241576454..6537b21d22 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -851,7 +851,7 @@ impl CodeActionsItem { pub fn label(&self) -> String { match self { - Self::CodeAction { action, .. } => action.lsp_action.title.clone(), + Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(), Self::Task(_, task) => task.resolved_label.clone(), } } @@ -984,7 +984,7 @@ impl CodeActionsMenu { .overflow_hidden() .child( // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. - action.lsp_action.title.replace("\n", ""), + action.lsp_action.title().replace("\n", ""), ) .when(selected, |this| { this.text_color(colors.text_accent) @@ -1029,7 +1029,7 @@ impl CodeActionsMenu { .max_by_key(|(_, action)| match action { CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(), CodeActionsItem::CodeAction { action, .. } => { - action.lsp_action.title.chars().count() + action.lsp_action.title().chars().count() } }) .map(|(ix, _)| ix), diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e646ada627..d2bfbc10c5 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -679,6 +679,9 @@ impl LanguageServer { ..Default::default() }), apply_edit: Some(true), + execute_command: Some(ExecuteCommandClientCapabilities { + dynamic_registration: Some(false), + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 947af390fc..3083bc7047 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2,9 +2,10 @@ mod signature_help; use crate::{ lsp_store::{LocalLspStore, LspStore}, - CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, - InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, - LocationLink, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState, + ActionVariant, CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, + HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, + InlayHintTooltip, Location, LocationLink, MarkupContent, PrepareRenameResponse, + ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; @@ -2218,10 +2219,10 @@ impl LspCommand for GetCodeActions { async fn response_from_lsp( self, actions: Option, - _: Entity, + lsp_store: Entity, _: Entity, server_id: LanguageServerId, - _: AsyncApp, + cx: AsyncApp, ) -> Result> { let requested_kinds_set = if let Some(kinds) = self.kinds { Some(kinds.into_iter().collect::>()) @@ -2229,18 +2230,47 @@ impl LspCommand for GetCodeActions { None }; + 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(actions .unwrap_or_default() .into_iter() .filter_map(|entry| { - let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry else { - return None; + let lsp_action = match entry { + lsp::CodeActionOrCommand::CodeAction(lsp_action) => { + if let Some(command) = lsp_action.command.as_ref() { + if !available_commands.contains(&command.command) { + return None; + } + } + ActionVariant::Action(Box::new(lsp_action)) + } + lsp::CodeActionOrCommand::Command(command) => { + if available_commands.contains(&command.command) { + ActionVariant::Command(command) + } else { + return None; + } + } }; if let Some((requested_kinds, kind)) = - requested_kinds_set.as_ref().zip(lsp_action.kind.as_ref()) + requested_kinds_set.as_ref().zip(lsp_action.action_kind()) { - if !requested_kinds.contains(kind) { + if !requested_kinds.contains(&kind) { return None; } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 0a73c90d18..e7e2708e2e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11,8 +11,8 @@ use crate::{ toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, yarn::YarnPathStore, - CodeAction, Completion, CoreCompletion, Hover, InlayHint, ProjectItem as _, ProjectPath, - ProjectTransaction, ResolveState, Symbol, ToolchainStore, + ActionVariant, CodeAction, Completion, CoreCompletion, Hover, InlayHint, ProjectItem as _, + ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore, }; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; @@ -1666,15 +1666,21 @@ impl LocalLspStore { lang_server: &LanguageServer, action: &mut CodeAction, ) -> anyhow::Result<()> { - if GetCodeActions::can_resolve_actions(&lang_server.capabilities()) - && 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?; + match &mut action.lsp_action { + ActionVariant::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()) + { + *lsp_action = Box::new( + lang_server + .request::(*lsp_action.clone()) + .await?, + ); + } + } + ActionVariant::Command(_) => {} } - anyhow::Ok(()) } @@ -2102,7 +2108,7 @@ impl LocalLspStore { } async fn execute_code_actions_on_servers( - this: &WeakEntity, + lsp_store: &WeakEntity, adapters_and_servers: &[(Arc, Arc)], code_actions: Vec, buffer: &Entity, @@ -2113,7 +2119,7 @@ impl LocalLspStore { for (lsp_adapter, language_server) in adapters_and_servers.iter() { let code_actions = code_actions.clone(); - let actions = this + let actions = lsp_store .update(cx, move |this, cx| { let request = GetCodeActions { range: text::Anchor::MIN..text::Anchor::MAX, @@ -2129,14 +2135,14 @@ impl LocalLspStore { .await .context("resolving a formatting code action")?; - if let Some(edit) = action.lsp_action.edit { + if let Some(edit) = action.lsp_action.edit() { if edit.changes.is_none() && edit.document_changes.is_none() { continue; } let new = Self::deserialize_workspace_edit( - this.upgrade().ok_or_else(|| anyhow!("project dropped"))?, - edit, + lsp_store.upgrade().context("project dropped")?, + edit.clone(), push_to_history, lsp_adapter.clone(), language_server.clone(), @@ -2146,32 +2152,42 @@ impl LocalLspStore { project_transaction.0.extend(new.0); } - if let Some(command) = action.lsp_action.command { - this.update(cx, |this, _| { - if let LspStoreMode::Local(mode) = &mut this.mode { - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()); - } - })?; - - language_server - .request::(lsp::ExecuteCommandParams { - command: command.command, - arguments: command.arguments.unwrap_or_default(), - ..Default::default() - }) - .await?; - - this.update(cx, |this, _| { - if let LspStoreMode::Local(mode) = &mut this.mode { - project_transaction.0.extend( + if let Some(command) = action.lsp_action.command() { + 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(); + if available_commands.contains(&command.command) { + lsp_store.update(cx, |lsp_store, _| { + if let LspStoreMode::Local(mode) = &mut lsp_store.mode { mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()) - .unwrap_or_default() - .0, - ) - } - })?; + .remove(&language_server.server_id()); + } + })?; + + language_server + .request::(lsp::ExecuteCommandParams { + command: command.command.clone(), + arguments: command.arguments.clone().unwrap_or_default(), + ..Default::default() + }) + .await?; + + lsp_store.update(cx, |this, _| { + if let LspStoreMode::Local(mode) = &mut this.mode { + project_transaction.0.extend( + mode.last_workspace_edits_by_language_server + .remove(&language_server.server_id()) + .unwrap_or_default() + .0, + ) + } + })?; + } else { + log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command) + } } } } @@ -3896,17 +3912,17 @@ impl LspStore { self.language_server_for_local_buffer(buffer, action.server_id, cx) .map(|(adapter, server)| (adapter.clone(), server.clone())) }) else { - return Task::ready(Ok(Default::default())); + return Task::ready(Ok(ProjectTransaction::default())); }; cx.spawn(move |this, mut cx| async move { LocalLspStore::try_resolve_code_action(&lang_server, &mut action) .await .context("resolving a code action")?; - if let Some(edit) = action.lsp_action.edit { + if let Some(edit) = action.lsp_action.edit() { if edit.changes.is_some() || edit.document_changes.is_some() { return LocalLspStore::deserialize_workspace_edit( this.upgrade().ok_or_else(|| anyhow!("no app present"))?, - edit, + edit.clone(), push_to_history, lsp_adapter.clone(), lang_server.clone(), @@ -3916,31 +3932,41 @@ impl LspStore { } } - if let Some(command) = action.lsp_action.command { - this.update(&mut cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()); - })?; + if let Some(command) = action.lsp_action.command() { + let server_capabilities = lang_server.capabilities(); + let available_commands = server_capabilities + .execute_command_provider + .as_ref() + .map(|options| options.commands.as_slice()) + .unwrap_or_default(); + if available_commands.contains(&command.command) { + this.update(&mut cx, |this, _| { + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server + .remove(&lang_server.server_id()); + })?; - let result = lang_server - .request::(lsp::ExecuteCommandParams { - command: command.command, - arguments: command.arguments.unwrap_or_default(), - ..Default::default() - }) - .await; + let result = lang_server + .request::(lsp::ExecuteCommandParams { + command: command.command.clone(), + arguments: command.arguments.clone().unwrap_or_default(), + ..Default::default() + }) + .await; - result?; + result?; - return this.update(&mut cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()) - .unwrap_or_default() - }); + return this.update(&mut cx, |this, _| { + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server + .remove(&lang_server.server_id()) + .unwrap_or_default() + }); + } else { + log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command); + } } Ok(ProjectTransaction::default()) @@ -8158,11 +8184,23 @@ impl LspStore { } pub(crate) fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { + let (kind, lsp_action) = match &action.lsp_action { + ActionVariant::Action(code_action) => ( + proto::code_action::Kind::Action as i32, + serde_json::to_vec(code_action).unwrap(), + ), + ActionVariant::Command(command) => ( + proto::code_action::Kind::Command as i32, + serde_json::to_vec(command).unwrap(), + ), + }; + proto::CodeAction { server_id: action.server_id.0 as u64, start: Some(serialize_anchor(&action.range.start)), end: Some(serialize_anchor(&action.range.end)), - lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(), + lsp_action, + kind, } } @@ -8175,7 +8213,15 @@ impl LspStore { .end .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("invalid end"))?; - let lsp_action = serde_json::from_slice(&action.lsp_action)?; + let lsp_action = match proto::code_action::Kind::from_i32(action.kind) { + Some(proto::code_action::Kind::Action) => { + ActionVariant::Action(serde_json::from_slice(&action.lsp_action)?) + } + Some(proto::code_action::Kind::Command) => { + ActionVariant::Command(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, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d3ef60506f..09b495502f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -411,7 +411,48 @@ pub struct CodeAction { /// The range of the buffer where this code action is applicable. pub range: Range, /// The raw code action provided by the language server. - pub lsp_action: lsp::CodeAction, + /// Can be either an action or a command. + pub lsp_action: ActionVariant, +} + +/// An action sent back by a language server. +#[derive(Clone, Debug)] +pub enum ActionVariant { + /// An action with the full data, may have a command or may not. + /// May require resolving. + Action(Box), + /// A command data to run as an action. + Command(lsp::Command), +} + +impl ActionVariant { + pub fn title(&self) -> &str { + match self { + Self::Action(action) => &action.title, + Self::Command(command) => &command.title, + } + } + + fn action_kind(&self) -> Option { + match self { + Self::Action(action) => action.kind.clone(), + Self::Command(_) => Some(lsp::CodeActionKind::new("command")), + } + } + + fn edit(&self) -> Option<&lsp::WorkspaceEdit> { + match self { + Self::Action(action) => action.edit.as_ref(), + Self::Command(_) => None, + } + } + + fn command(&self) -> Option<&lsp::Command> { + match self { + Self::Action(action) => action.command.as_ref(), + Self::Command(command) => Some(command), + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ddaba39747..fed349736b 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2951,6 +2951,10 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { ..lsp::CodeActionOptions::default() }, )), + execute_command_provider: Some(lsp::ExecuteCommandOptions { + commands: vec!["_the/command".to_string()], + ..lsp::ExecuteCommandOptions::default() + }), ..lsp::ServerCapabilities::default() }, ..FakeLspAdapter::default() @@ -5372,7 +5376,7 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { let code_actions = code_actions_task.await.unwrap(); assert_eq!(code_actions.len(), 1); assert_eq!( - code_actions[0].lsp_action.kind, + code_actions[0].lsp_action.action_kind(), Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS) ); } @@ -5529,7 +5533,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { .await .unwrap() .into_iter() - .map(|code_action| code_action.lsp_action.title) + .map(|code_action| code_action.lsp_action.title().to_owned()) .sorted() .collect::>(), "Should receive code actions responses from all related servers with hover capabilities" diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 88bc218dc5..17bf7a36ac 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -1286,6 +1286,11 @@ message CodeAction { Anchor start = 2; Anchor end = 3; bytes lsp_action = 4; + Kind kind = 5; + enum Kind { + Action = 0; + Command = 1; + } } message ProjectTransaction {