Add `"code_actions_on_format"` (#7860)

Conrad Irwin and Thorsten created

This lets Go programmers configure `"code_actions_on_format": {
  "source.organizeImports": true,
}` so that they don't have to manage their imports manually

I landed on `code_actions_on_format` instead of `code_actions_on_save`
(the
VSCode version of this) because I want to run these when I explicitly
format
(and not if `format_on_save` is disabled).

Co-Authored-By: Thorsten <thorsten@zed.dev>

Release Notes:

- Added `"code_actions_on_format"` to control additional formatting
steps on format/save
([#5232](https://github.com/zed-industries/zed/issues/5232)).
- Added a `"code_actions_on_format"` of `"source.organizeImports"` for
Go ([#4886](https://github.com/zed-industries/zed/issues/4886)).

Co-authored-by: Thorsten <thorsten@zed.dev>

Change summary

assets/settings/default.json             |   6 
crates/editor/src/items.rs               |   8 
crates/language/src/language_settings.rs |  11 +
crates/project/src/lsp_command.rs        |  11 +
crates/project/src/project.rs            | 198 +++++++++++++++++++++++++
5 files changed, 221 insertions(+), 13 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -482,6 +482,7 @@
   "deno": {
     "enable": false
   },
+  "code_actions_on_format": {},
   // Different settings for specific languages.
   "languages": {
     "Plain Text": {
@@ -492,7 +493,10 @@
     },
     "Go": {
       "tab_size": 4,
-      "hard_tabs": true
+      "hard_tabs": true,
+      "code_actions_on_format": {
+        "source.organizeImports": true
+      }
     },
     "Markdown": {
       "soft_wrap": "preferred_line_length"

crates/editor/src/items.rs 🔗

@@ -704,10 +704,12 @@ impl Item for Editor {
 
     fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
         self.report_editor_event("save", None, cx);
-        let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
         let buffers = self.buffer().clone().read(cx).all_buffers();
-        cx.spawn(|_, mut cx| async move {
-            format.await?;
+        cx.spawn(|this, mut cx| async move {
+            this.update(&mut cx, |this, cx| {
+                this.perform_format(project.clone(), FormatTrigger::Save, cx)
+            })?
+            .await?;
 
             if buffers.len() == 1 {
                 project

crates/language/src/language_settings.rs 🔗

@@ -93,6 +93,8 @@ pub struct LanguageSettings {
     pub inlay_hints: InlayHintSettings,
     /// Whether to automatically close brackets.
     pub use_autoclose: bool,
+    /// Which code actions to run on save
+    pub code_actions_on_format: HashMap<String, bool>,
 }
 
 /// The settings for [GitHub Copilot](https://github.com/features/copilot).
@@ -215,6 +217,11 @@ pub struct LanguageSettingsContent {
     ///
     /// Default: true
     pub use_autoclose: Option<bool>,
+
+    /// Which code actions to run on save
+    ///
+    /// Default: {} (or {"source.organizeImports": true} for Go).
+    pub code_actions_on_format: Option<HashMap<String, bool>>,
 }
 
 /// The contents of the GitHub Copilot settings.
@@ -550,6 +557,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
     merge(&mut settings.use_autoclose, src.use_autoclose);
     merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
     merge(&mut settings.wrap_guides, src.wrap_guides.clone());
+    merge(
+        &mut settings.code_actions_on_format,
+        src.code_actions_on_format.clone(),
+    );
 
     merge(
         &mut settings.preferred_line_length,

crates/project/src/lsp_command.rs 🔗

@@ -123,6 +123,7 @@ pub(crate) struct GetCompletions {
 
 pub(crate) struct GetCodeActions {
     pub range: Range<Anchor>,
+    pub kinds: Option<Vec<lsp::CodeActionKind>>,
 }
 
 pub(crate) struct OnTypeFormatting {
@@ -1603,7 +1604,10 @@ impl LspCommand for GetCodeActions {
             partial_result_params: Default::default(),
             context: lsp::CodeActionContext {
                 diagnostics: relevant_diagnostics,
-                only: language_server.code_action_kinds(),
+                only: self
+                    .kinds
+                    .clone()
+                    .or_else(|| language_server.code_action_kinds()),
                 ..lsp::CodeActionContext::default()
             },
         }
@@ -1664,7 +1668,10 @@ impl LspCommand for GetCodeActions {
             })?
             .await?;
 
-        Ok(Self { range: start..end })
+        Ok(Self {
+            range: start..end,
+            kinds: None,
+        })
     }
 
     fn response_to_proto(

crates/project/src/project.rs 🔗

@@ -4150,10 +4150,11 @@ impl Project {
                     let buffer = buffer_handle.read(cx);
                     let file = File::from_dyn(buffer.file())?;
                     let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
-                    let server = self
+                    let (adapter, server) = self
                         .primary_language_server_for_buffer(buffer, cx)
-                        .map(|s| s.1.clone());
-                    Some((buffer_handle, buffer_abs_path, server))
+                        .map(|(a, s)| (Some(a.clone()), Some(s.clone())))
+                        .unwrap_or((None, None));
+                    Some((buffer_handle, buffer_abs_path, adapter, server))
                 })
                 .collect::<Vec<_>>();
 
@@ -4161,7 +4162,7 @@ impl Project {
                 // Do not allow multiple concurrent formatting requests for the
                 // same buffer.
                 project.update(&mut cx, |this, cx| {
-                    buffers_with_paths_and_servers.retain(|(buffer, _, _)| {
+                    buffers_with_paths_and_servers.retain(|(buffer, _, _, _)| {
                         this.buffers_being_formatted
                             .insert(buffer.read(cx).remote_id())
                     });
@@ -4173,7 +4174,7 @@ impl Project {
                     let buffers = &buffers_with_paths_and_servers;
                     move || {
                         this.update(&mut cx, |this, cx| {
-                            for (buffer, _, _) in buffers {
+                            for (buffer, _, _, _) in buffers {
                                 this.buffers_being_formatted
                                     .remove(&buffer.read(cx).remote_id());
                             }
@@ -4183,7 +4184,9 @@ impl Project {
                 });
 
                 let mut project_transaction = ProjectTransaction::default();
-                for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
+                for (buffer, buffer_abs_path, lsp_adapter, language_server) in
+                    &buffers_with_paths_and_servers
+                {
                     let settings = buffer.update(&mut cx, |buffer, cx| {
                         language_settings(buffer.language(), buffer.file(), cx).clone()
                     })?;
@@ -4214,6 +4217,88 @@ impl Project {
                         buffer.end_transaction(cx)
                     })?;
 
+                    if let (Some(lsp_adapter), Some(language_server)) =
+                        (lsp_adapter, language_server)
+                    {
+                        // Apply the code actions on
+                        let code_actions: Vec<lsp::CodeActionKind> = settings
+                            .code_actions_on_format
+                            .iter()
+                            .flat_map(|(kind, enabled)| {
+                                if *enabled {
+                                    Some(kind.clone().into())
+                                } else {
+                                    None
+                                }
+                            })
+                            .collect();
+
+                        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 action in actions {
+                                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::request::ExecuteCommand>(
+                                            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 language-specific formatting using either a language server
                     // or external command.
                     let mut format_operation = None;
@@ -4323,6 +4408,8 @@ impl Project {
 
                             if let Some(transaction_id) = whitespace_transaction_id {
                                 b.group_until_transaction(transaction_id);
+                            } else if let Some(transaction) = project_transaction.0.get(buffer) {
+                                b.group_until_transaction(transaction.id)
                             }
                         }
 
@@ -5162,7 +5249,7 @@ impl Project {
         self.request_lsp(
             buffer_handle.clone(),
             LanguageServerToQuery::Primary,
-            GetCodeActions { range },
+            GetCodeActions { range, kinds: None },
             cx,
         )
     }
@@ -5178,6 +5265,103 @@ impl Project {
         self.code_actions_impl(buffer_handle, range, cx)
     }
 
+    pub fn apply_code_actions_on_save(
+        &self,
+        buffers: HashSet<Model<Buffer>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ProjectTransaction>> {
+        if !self.is_local() {
+            return Task::ready(Ok(Default::default()));
+        }
+
+        let buffers_with_adapters_and_servers = buffers
+            .into_iter()
+            .filter_map(|buffer_handle| {
+                let buffer = buffer_handle.read(cx);
+                self.primary_language_server_for_buffer(buffer, cx)
+                    .map(|(a, s)| (buffer_handle, a.clone(), s.clone()))
+            })
+            .collect::<Vec<_>>();
+
+        cx.spawn(move |this, mut cx| async move {
+            for (buffer_handle, lsp_adapter, lang_server) in buffers_with_adapters_and_servers {
+                let actions = this
+                    .update(&mut cx, |this, cx| {
+                        let buffer = buffer_handle.read(cx);
+                        let kinds: Vec<lsp::CodeActionKind> =
+                            language_settings(buffer.language(), buffer.file(), cx)
+                                .code_actions_on_format
+                                .iter()
+                                .flat_map(|(kind, enabled)| {
+                                    if *enabled {
+                                        Some(kind.clone().into())
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .collect();
+                        if kinds.is_empty() {
+                            return Task::ready(Ok(vec![]));
+                        }
+
+                        this.request_lsp(
+                            buffer_handle.clone(),
+                            LanguageServerToQuery::Other(lang_server.server_id()),
+                            GetCodeActions {
+                                range: text::Anchor::MIN..text::Anchor::MAX,
+                                kinds: Some(kinds),
+                            },
+                            cx,
+                        )
+                    })?
+                    .await?;
+
+                for action in actions {
+                    if let Some(edit) = action.lsp_action.edit {
+                        if edit.changes.is_some() || edit.document_changes.is_some() {
+                            return Self::deserialize_workspace_edit(
+                                this.upgrade().ok_or_else(|| anyhow!("no app present"))?,
+                                edit,
+                                true,
+                                lsp_adapter.clone(),
+                                lang_server.clone(),
+                                &mut cx,
+                            )
+                            .await;
+                        }
+                    }
+
+                    if let Some(command) = action.lsp_action.command {
+                        this.update(&mut cx, |this, _| {
+                            this.last_workspace_edits_by_language_server
+                                .remove(&lang_server.server_id());
+                        })?;
+
+                        let result = lang_server
+                            .request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
+                                command: command.command,
+                                arguments: command.arguments.unwrap_or_default(),
+                                ..Default::default()
+                            })
+                            .await;
+
+                        if let Err(err) = result {
+                            // TODO: LSP ERROR
+                            return Err(err);
+                        }
+
+                        return Ok(this.update(&mut cx, |this, _| {
+                            this.last_workspace_edits_by_language_server
+                                .remove(&lang_server.server_id())
+                                .unwrap_or_default()
+                        })?);
+                    }
+                }
+            }
+            Ok(ProjectTransaction::default())
+        })
+    }
+
     pub fn apply_code_action(
         &self,
         buffer_handle: Model<Buffer>,