diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 3877614179..96d57adc86 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -241,7 +241,8 @@ pub struct LanguageSettingsContent { /// /// Default: false pub always_treat_brackets_as_autoclosed: Option, - /// Which code actions to run on save + /// Which code actions to run on save after the formatter. + /// These are not run if formatting is off. /// /// Default: {} (or {"source.organizeImports": true} for Go). pub code_actions_on_format: Option>, @@ -292,6 +293,8 @@ pub enum FormatOnSave { /// The arguments to pass to the program. arguments: Arc<[String]>, }, + /// Files should be formatted using code actions executed by language servers. + CodeActions(HashMap), } /// Controls how whitespace should be displayedin the editor. @@ -325,6 +328,8 @@ pub enum Formatter { /// The arguments to pass to the program. arguments: Arc<[String]>, }, + /// Files should be formatted using code actions executed by language servers. + CodeActions(HashMap), } /// The settings for inlay hints. diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index 22463f7210..d34a74e013 100644 --- a/crates/project/src/prettier_support.rs +++ b/crates/project/src/prettier_support.rs @@ -31,7 +31,9 @@ pub fn prettier_plugins_for_language<'a>( ) -> Option<&'a Vec>> { match &language_settings.formatter { Formatter::Prettier { .. } | Formatter::Auto => {} - Formatter::LanguageServer | Formatter::External { .. } => return None, + Formatter::LanguageServer | Formatter::External { .. } | Formatter::CodeActions(_) => { + return None + } }; if language.prettier_parser_name().is_some() { Some(language.prettier_plugins()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cbabda7417..f9dbc3f122 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4539,93 +4539,27 @@ impl Project { buffer.end_transaction(cx) })?; - for (lsp_adapter, language_server) in adapters_and_servers.iter() { - // Apply the code actions on - let code_actions: Vec = settings - .code_actions_on_format - .iter() - .flat_map(|(kind, enabled)| { - if *enabled { - Some(kind.clone().into()) - } else { - None - } - }) - .collect(); - - #[allow(clippy::nonminimal_bool)] - if !code_actions.is_empty() - && !(trigger == FormatTrigger::Save - && settings.format_on_save == FormatOnSave::Off) - { - let actions = project - .update(&mut cx, |this, cx| { - this.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(language_server.server_id()), - GetCodeActions { - range: text::Anchor::MIN..text::Anchor::MAX, - kinds: Some(code_actions), - }, - cx, - ) - })? - .await?; - - 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; - } - - let new = Self::deserialize_workspace_edit( - project - .upgrade() - .ok_or_else(|| anyhow!("project dropped"))?, - edit, - push_to_history, - lsp_adapter.clone(), - language_server.clone(), - &mut cx, - ) - .await?; - project_transaction.0.extend(new.0); - } - - if let Some(command) = action.lsp_action.command { - project.update(&mut cx, |this, _| { - this.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?; - - project.update(&mut cx, |this, _| { - project_transaction.0.extend( - this.last_workspace_edits_by_language_server - .remove(&language_server.server_id()) - .unwrap_or_default() - .0, - ) - })?; - } - } - } + // Apply the `code_actions_on_format` before we run the formatter. + let code_actions = deserialize_code_actions(&settings.code_actions_on_format); + #[allow(clippy::nonminimal_bool)] + if !code_actions.is_empty() + && !(trigger == FormatTrigger::Save && settings.format_on_save == FormatOnSave::Off) + { + Self::execute_code_actions_on_servers( + &project, + &adapters_and_servers, + code_actions, + buffer, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await?; } // Apply language-specific formatting using either the primary language server // or external command. + // Except for code actions, which are applied with all connected language servers. let primary_language_server = adapters_and_servers .first() .cloned() @@ -4638,6 +4572,22 @@ impl Project { match (&settings.formatter, &settings.format_on_save) { (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {} + (Formatter::CodeActions(code_actions), FormatOnSave::On | FormatOnSave::Off) + | (_, FormatOnSave::CodeActions(code_actions)) => { + let code_actions = deserialize_code_actions(code_actions); + if !code_actions.is_empty() { + Self::execute_code_actions_on_servers( + &project, + &adapters_and_servers, + code_actions, + buffer, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await?; + } + } (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off) | (_, FormatOnSave::LanguageServer) => { if let Some((language_server, buffer_abs_path)) = server_and_buffer { @@ -8832,6 +8782,82 @@ impl Project { anyhow::Ok(()) } + async fn execute_code_actions_on_servers( + project: &WeakModel, + adapters_and_servers: &Vec<(Arc, Arc)>, + code_actions: Vec, + buffer: &Model, + push_to_history: bool, + project_transaction: &mut ProjectTransaction, + cx: &mut AsyncAppContext, + ) -> Result<(), anyhow::Error> { + for (lsp_adapter, language_server) in adapters_and_servers.iter() { + let code_actions = code_actions.clone(); + + let actions = project + .update(cx, move |this, cx| { + let request = GetCodeActions { + range: text::Anchor::MIN..text::Anchor::MAX, + kinds: Some(code_actions), + }; + let server = LanguageServerToQuery::Other(language_server.server_id()); + this.request_lsp(buffer.clone(), server, request, cx) + })? + .await?; + + 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; + } + + let new = Self::deserialize_workspace_edit( + project + .upgrade() + .ok_or_else(|| anyhow!("project dropped"))?, + edit, + push_to_history, + lsp_adapter.clone(), + language_server.clone(), + cx, + ) + .await?; + project_transaction.0.extend(new.0); + } + + if let Some(command) = action.lsp_action.command { + project.update(cx, |this, _| { + this.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?; + + project.update(cx, |this, _| { + project_transaction.0.extend( + this.last_workspace_edits_by_language_server + .remove(&language_server.server_id()) + .unwrap_or_default() + .0, + ) + })?; + } + } + } + + Ok(()) + } + async fn handle_refresh_inlay_hints( this: Model, _: TypedEnvelope, @@ -9671,6 +9697,19 @@ impl Project { } } +fn deserialize_code_actions(code_actions: &HashMap) -> Vec { + code_actions + .iter() + .flat_map(|(kind, enabled)| { + if *enabled { + Some(kind.clone().into()) + } else { + None + } + }) + .collect() +} + #[allow(clippy::too_many_arguments)] async fn search_snapshots( snapshots: &Vec,