Deprecate code actions on format setting (#39983)

Ben Kunkle created

Closes #ISSUE

Release Notes:

- settings: Deprecated `code_actions_on_format` in favor of specifying
code actions to run on format inline in the `formatter` array.

Previously, you would configure code actions to run on format like this:

```json
{
  "code_actions_on_format": {
    "source.organizeImports": true,
    "source.fixAll.eslint": true
  }
}
```

This has been migrated to the new format:

```json
{
  "formatter": [
    {
      "code_action": "source.organizeImports"
    },
    {
      "code_action": "source.fixAll.eslint"
    }
  ]
}
```

This change will be automatically migrated for you. If you had an
existing `formatter` setting, the code actions are prepended to your
formatter array (matching the existing behavior). This migration applies
to both global settings and language-specific settings

Change summary

assets/settings/default.json                            |   7 
crates/language/src/language_settings.rs                |   3 
crates/migrator/src/migrations.rs                       |   6 
crates/migrator/src/migrations/m_2025_10_10/settings.rs |  70 ++
crates/migrator/src/migrator.rs                         | 299 +++++++++++
crates/project/src/lsp_store.rs                         |  31 -
crates/settings/src/settings_content/language.rs        |   5 
crates/settings_ui/src/page_data.rs                     |  21 
8 files changed, 377 insertions(+), 65 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1101,7 +1101,7 @@
   // Removes any lines containing only whitespace at the end of the file and
   // ensures just one newline at the end.
   "ensure_final_newline_on_save": true,
-  // Whether or not to perform a buffer format before saving: [on, off, prettier, language_server]
+  // Whether or not to perform a buffer format before saving: [on, off]
   // Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
   "format_on_save": "on",
   // How to perform a buffer format. This setting can take 4 values:
@@ -1515,7 +1515,6 @@
     // A value of 45 preserves colorful themes while ensuring legibility.
     "minimum_contrast": 45
   },
-  "code_actions_on_format": {},
   // Settings related to running tasks.
   "tasks": {
     "variables": {},
@@ -1685,9 +1684,7 @@
       "preferred_line_length": 72
     },
     "Go": {
-      "code_actions_on_format": {
-        "source.organizeImports": true
-      },
+      "formatter": [{ "code_action": "source.organizeImports" }, { "language_server": {} }],
       "debuggers": ["Delve"]
     },
     "GraphQL": {

crates/language/src/language_settings.rs 🔗

@@ -142,8 +142,6 @@ pub struct LanguageSettings {
     pub auto_indent_on_paste: bool,
     /// Controls how the editor handles the autoclosed characters.
     pub always_treat_brackets_as_autoclosed: bool,
-    /// Which code actions to run on save
-    pub code_actions_on_format: HashMap<String, bool>,
     /// Whether to perform linked edits
     pub linked_edits: bool,
     /// Task configuration for this language.
@@ -568,7 +566,6 @@ impl settings::Settings for AllLanguageSettings {
                 always_treat_brackets_as_autoclosed: settings
                     .always_treat_brackets_as_autoclosed
                     .unwrap(),
-                code_actions_on_format: settings.code_actions_on_format.unwrap(),
                 linked_edits: settings.linked_edits.unwrap(),
                 tasks: LanguageTaskSettings {
                     variables: tasks.variables.unwrap_or_default(),

crates/migrator/src/migrations.rs 🔗

@@ -117,3 +117,9 @@ pub(crate) mod m_2025_10_03 {
 
     pub(crate) use settings::SETTINGS_PATTERNS;
 }
+
+pub(crate) mod m_2025_10_10 {
+    mod settings;
+
+    pub(crate) use settings::remove_code_actions_on_format;
+}

crates/migrator/src/migrations/m_2025_10_10/settings.rs 🔗

@@ -0,0 +1,70 @@
+use anyhow::Result;
+use serde_json::Value;
+
+pub fn remove_code_actions_on_format(value: &mut Value) -> Result<()> {
+    remove_code_actions_on_format_inner(value, &[])?;
+    let languages = value
+        .as_object_mut()
+        .and_then(|obj| obj.get_mut("languages"))
+        .and_then(|languages| languages.as_object_mut());
+    if let Some(languages) = languages {
+        for (language_name, language) in languages.iter_mut() {
+            let path = vec!["languages", language_name];
+            remove_code_actions_on_format_inner(language, &path)?;
+        }
+    }
+    Ok(())
+}
+
+fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
+    let Some(obj) = value.as_object_mut() else {
+        return Ok(());
+    };
+    let Some(code_actions_on_format) = obj.get("code_actions_on_format").cloned() else {
+        return Ok(());
+    };
+
+    fn fmt_path(path: &[&str], key: &str) -> String {
+        let mut path = path.to_vec();
+        path.push(key);
+        path.join(".")
+    }
+
+    anyhow::ensure!(
+        code_actions_on_format.is_object(),
+        r#"The `code_actions_on_format` setting is deprecated, but it is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
+        fmt_path(path, "code_actions_on_format"),
+    );
+
+    let code_actions_map = code_actions_on_format.as_object().unwrap();
+    let mut code_actions = vec![];
+    for (code_action, code_action_enabled) in code_actions_map {
+        if code_action_enabled.as_bool().map_or(false, |b| !b) {
+            continue;
+        }
+        code_actions.push(code_action.clone());
+    }
+
+    let mut formatter_array = vec![];
+    if let Some(formatter) = obj.get("formatter") {
+        if let Some(array) = formatter.as_array() {
+            formatter_array = array.clone();
+        } else {
+            formatter_array.insert(0, formatter.clone());
+        }
+    };
+    let found_code_actions = !code_actions.is_empty();
+    formatter_array.splice(
+        0..0,
+        code_actions
+            .into_iter()
+            .map(|code_action| serde_json::json!({"code_action": code_action})),
+    );
+
+    obj.remove("code_actions_on_format");
+    if found_code_actions {
+        obj.insert("formatter".to_string(), Value::Array(formatter_array));
+    }
+
+    Ok(())
+}

crates/migrator/src/migrator.rs 🔗

@@ -213,6 +213,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
             migrations::m_2025_10_03::SETTINGS_PATTERNS,
             &SETTINGS_QUERY_2025_10_03,
         ),
+        MigrationType::Json(migrations::m_2025_10_10::remove_code_actions_on_format),
     ];
     run_migrations(text, migrations)
 }
@@ -1913,4 +1914,302 @@ mod tests {
             None,
         );
     }
+
+    #[test]
+    fn test_code_actions_on_format_migration_basic() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+                "code_actions_on_format": {
+                    "source.organizeImports": true,
+                    "source.fixAll": true
+                }
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                    "formatter": [
+                        {
+                            "code_action": "source.organizeImports"
+                        },
+                        {
+                            "code_action": "source.fixAll"
+                        }
+                    ]
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_migration_filters_false_values() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+                "code_actions_on_format": {
+                    "a": true,
+                    "b": false,
+                    "c": true
+                }
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                    "formatter": [
+                        {
+                            "code_action": "a"
+                        },
+                        {
+                            "code_action": "c"
+                        }
+                    ]
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_migration_with_existing_formatter_object() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+              "formatter": "prettier",
+              "code_actions_on_format": {
+                "source.organizeImports": true
+              }
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                  "formatter": [
+                    {
+                      "code_action": "source.organizeImports"
+                    },
+                    "prettier"
+                  ]
+                }"#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_migration_with_existing_formatter_array() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+              "formatter": ["prettier", {"language_server": "eslint"}],
+              "code_actions_on_format": {
+                "source.organizeImports": true,
+                "source.fixAll": true
+              }
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                  "formatter": [
+                    {
+                      "code_action": "source.organizeImports"
+                    },
+                    {
+                      "code_action": "source.fixAll"
+                    },
+                    "prettier",
+                    {
+                      "language_server": "eslint"
+                    }
+                  ]
+                }"#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_migration_in_languages() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+                "languages": {
+                    "JavaScript": {
+                        "code_actions_on_format": {
+                            "source.fixAll.eslint": true
+                        }
+                    },
+                    "Go": {
+                        "code_actions_on_format": {
+                            "source.organizeImports": true
+                        }
+                    }
+                }
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                    "languages": {
+                        "JavaScript": {
+                            "formatter": [
+                                {
+                                    "code_action": "source.fixAll.eslint"
+                                }
+                            ]
+                        },
+                        "Go": {
+                            "formatter": [
+                                {
+                                    "code_action": "source.organizeImports"
+                                }
+                            ]
+                        }
+                    }
+                }"#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_migration_in_languages_with_existing_formatter() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+              "languages": {
+                "JavaScript": {
+                  "formatter": "prettier",
+                  "code_actions_on_format": {
+                    "source.fixAll.eslint": true,
+                    "source.organizeImports": false
+                  }
+                }
+              }
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                  "languages": {
+                    "JavaScript": {
+                      "formatter": [
+                        {
+                          "code_action": "source.fixAll.eslint"
+                        },
+                        "prettier"
+                      ]
+                    }
+                  }
+                }"#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_migration_mixed_global_and_languages() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+              "formatter": "prettier",
+              "code_actions_on_format": {
+                "source.fixAll": true
+              },
+              "languages": {
+                "Rust": {
+                  "formatter": "rust-analyzer",
+                  "code_actions_on_format": {
+                    "source.organizeImports": true
+                  }
+                },
+                "Python": {
+                  "code_actions_on_format": {
+                    "source.organizeImports": true,
+                    "source.fixAll": false
+                  }
+                }
+              }
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                  "formatter": [
+                    {
+                      "code_action": "source.fixAll"
+                    },
+                    "prettier"
+                  ],
+                  "languages": {
+                    "Rust": {
+                      "formatter": [
+                        {
+                          "code_action": "source.organizeImports"
+                        },
+                        "rust-analyzer"
+                      ]
+                    },
+                    "Python": {
+                            "formatter": [
+                                {
+                                    "code_action": "source.organizeImports"
+                                }
+                            ]
+                        }
+                  }
+                }"#
+                .unindent(),
+            ),
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_no_migration_when_not_present() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+              "formatter": ["prettier"]
+            }"#
+            .unindent(),
+            None,
+        );
+    }
+
+    #[test]
+    fn test_code_actions_on_format_migration_all_false_values() {
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::Json(
+                migrations::m_2025_10_10::remove_code_actions_on_format,
+            )],
+            &r#"{
+                "code_actions_on_format": {
+                    "a": false,
+                    "b": false
+                },
+                "formatter": "prettier"
+            }"#
+            .unindent(),
+            Some(
+                &r#"{
+                    "formatter": "prettier"
+                }"#
+                .unindent(),
+            ),
+        );
+    }
 }

crates/project/src/lsp_store.rs 🔗

@@ -1338,32 +1338,6 @@ impl LocalLspStore {
             })?;
         }
 
-        // Formatter for `code_actions_on_format` that runs before
-        // the rest of the formatters
-        let mut code_actions_on_format_formatters = None;
-        let should_run_code_actions_on_format = !matches!(
-            (trigger, &settings.format_on_save),
-            (FormatTrigger::Save, &FormatOnSave::Off)
-        );
-        if should_run_code_actions_on_format {
-            let have_code_actions_to_run_on_format = settings
-                .code_actions_on_format
-                .values()
-                .any(|enabled| *enabled);
-            if have_code_actions_to_run_on_format {
-                zlog::trace!(logger => "going to run code actions on format");
-                code_actions_on_format_formatters = Some(
-                    settings
-                        .code_actions_on_format
-                        .iter()
-                        .filter_map(|(action, enabled)| enabled.then_some(action))
-                        .cloned()
-                        .map(Formatter::CodeAction)
-                        .collect::<Vec<_>>(),
-                );
-            }
-        }
-
         let formatters = match (trigger, &settings.format_on_save) {
             (FormatTrigger::Save, FormatOnSave::Off) => &[],
             (FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
@@ -1382,11 +1356,6 @@ impl LocalLspStore {
             }
         };
 
-        let formatters = code_actions_on_format_formatters
-            .iter()
-            .flatten()
-            .chain(formatters);
-
         for formatter in formatters {
             match formatter {
                 Formatter::Prettier => {

crates/settings/src/settings_content/language.rs 🔗

@@ -304,11 +304,6 @@ pub struct LanguageSettingsContent {
     ///
     /// Default: true
     pub use_on_type_format: Option<bool>,
-    /// 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<HashMap<String, bool>>,
     /// Whether to perform linked edits of associated ranges, if the language server supports it.
     /// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
     ///

crates/settings_ui/src/page_data.rs 🔗

@@ -4840,27 +4840,6 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
             metadata: None,
             files: USER | LOCAL,
         }),
-        SettingsPageItem::SettingItem(SettingItem {
-            title: "Code Actions On Format",
-            description: "Which code actions to run on save after the formatter. These are not run if formatting is off",
-            field: Box::new(
-                SettingField {
-                    pick: |settings_content| {
-                        language_settings_field(settings_content, |language| {
-                            &language.code_actions_on_format
-                        })
-                    },
-                    pick_mut: |settings_content| {
-                        language_settings_field_mut(settings_content, |language| {
-                            &mut language.code_actions_on_format
-                        })
-                    },
-                }
-                .unimplemented(),
-            ),
-            metadata: None,
-            files: USER | LOCAL,
-        }),
         SettingsPageItem::SectionHeader("Prettier"),
         SettingsPageItem::SettingItem(SettingItem {
             title: "Allowed",