Revert deprecate code actions on format (#40409)

Ben Kunkle , Cole Miller , Mikayla Maki , and HactarCE created

Closes #40334

This reverts the change made in #39983, and includes a replacement
migration that will transform formatter settings values consisting of
only `code_action` format steps into the previously deprecated
`code_actions_on_format` in an attempt to restore the behavior to what
it was before the migration that deprecated `code_actions_on_format`.

This PR will result in a modified order in the `code_actions_on_format`
setting if it existed, however the decision was made to explicitly
ignore this for now, as this PR is primarily targeting users who have
already had the deprecation migration run, and no longer have the
`code_actions_on_format` key

Release Notes:

- Fixed an issue with a settings migration that deprecated the
`code_actions_on_format` setting. The `code_actions_on_format` setting
has been un-deprecated, and affected users will have the bad migration
rolled back with an updated migration

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>

Change summary

assets/settings/default.json                            |   5 
crates/language/src/language_settings.rs                |   3 
crates/migrator/src/migrations.rs                       |   4 
crates/migrator/src/migrations/m_2025_10_10/settings.rs |  70 --
crates/migrator/src/migrations/m_2025_10_16/settings.rs |  68 ++
crates/migrator/src/migrator.rs                         | 301 +---------
crates/migrator/src/patterns.rs                         |   1 
crates/migrator/src/patterns/settings.rs                |  21 
crates/project/src/lsp_store.rs                         |  31 +
crates/settings/src/settings_content/language.rs        |   5 
crates/settings_ui/src/page_data.rs                     |  21 
docs/src/languages/javascript.md                        |   6 
12 files changed, 212 insertions(+), 324 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1521,6 +1521,7 @@
     // A value of 45 preserves colorful themes while ensuring legibility.
     "minimum_contrast": 45
   },
+  "code_actions_on_format": {},
   // Settings related to running tasks.
   "tasks": {
     "variables": {},
@@ -1690,7 +1691,9 @@
       "preferred_line_length": 72
     },
     "Go": {
-      "formatter": [{ "code_action": "source.organizeImports" }, "language_server"],
+      "code_actions_on_format": {
+        "source.organizeImports": true
+      },
       "debuggers": ["Delve"]
     },
     "GraphQL": {

crates/language/src/language_settings.rs 🔗

@@ -142,6 +142,8 @@ 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.
@@ -566,6 +568,7 @@ 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 🔗

@@ -118,8 +118,8 @@ pub(crate) mod m_2025_10_03 {
     pub(crate) use settings::SETTINGS_PATTERNS;
 }
 
-pub(crate) mod m_2025_10_10 {
+pub(crate) mod m_2025_10_16 {
     mod settings;
 
-    pub(crate) use settings::remove_code_actions_on_format;
+    pub(crate) use settings::restore_code_actions_on_format;
 }

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

@@ -1,70 +0,0 @@
-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/migrations/m_2025_10_16/settings.rs 🔗

@@ -0,0 +1,68 @@
+use anyhow::Result;
+use serde_json::Value;
+
+use crate::patterns::migrate_language_setting;
+
+pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> {
+    migrate_language_setting(value, restore_code_actions_on_format_inner)
+}
+
+fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
+    let Some(obj) = value.as_object_mut() else {
+        return Ok(());
+    };
+    let code_actions_on_format = obj
+        .get("code_actions_on_format")
+        .cloned()
+        .unwrap_or_else(|| Value::Object(Default::default()));
+
+    fn fmt_path(path: &[&str], key: &str) -> String {
+        let mut path = path.to_vec();
+        path.push(key);
+        path.join(".")
+    }
+
+    let Some(mut code_actions_map) = code_actions_on_format.as_object().cloned() else {
+        anyhow::bail!(
+            r#"The `code_actions_on_format` 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 Some(formatter) = obj.get("formatter") else {
+        return Ok(());
+    };
+    let formatter_array = if let Some(array) = formatter.as_array() {
+        array.clone()
+    } else {
+        vec![formatter.clone()]
+    };
+    let mut code_action_formatters = Vec::new();
+    for formatter in formatter_array {
+        let Some(code_action) = formatter.get("code_action") else {
+            return Ok(());
+        };
+        let Some(code_action_name) = code_action.as_str() else {
+            anyhow::bail!(
+                r#"The `code_action` is in an invalid state and cannot be migrated at {}. Please ensure the code_action setting is a String"#,
+                fmt_path(path, "formatter"),
+            );
+        };
+        code_action_formatters.push(code_action_name.to_string());
+    }
+
+    code_actions_map.extend(
+        code_action_formatters
+            .into_iter()
+            .rev()
+            .map(|code_action| (code_action, Value::Bool(true))),
+    );
+
+    obj.remove("formatter");
+    obj.insert(
+        "code_actions_on_format".into(),
+        Value::Object(code_actions_map),
+    );
+
+    Ok(())
+}

crates/migrator/src/migrator.rs 🔗

@@ -213,7 +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),
+        MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
     ];
     run_migrations(text, migrations)
 }
@@ -367,6 +367,7 @@ mod tests {
         pretty_assertions::assert_eq!(migrated.as_deref(), output);
     }
 
+    #[track_caller]
     fn assert_migrate_settings(input: &str, output: Option<&str>) {
         let migrated = migrate_settings(input).unwrap();
         assert_migrated_correctly(migrated, output);
@@ -1341,7 +1342,11 @@ mod tests {
 
     #[test]
     fn test_flatten_code_action_formatters_basic_array() {
-        assert_migrate_settings(
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::TreeSitter(
+                migrations::m_2025_10_01::SETTINGS_PATTERNS,
+                &SETTINGS_QUERY_2025_10_01,
+            )],
             &r#"{
                 "formatter": [
                   {
@@ -1368,7 +1373,11 @@ mod tests {
 
     #[test]
     fn test_flatten_code_action_formatters_basic_object() {
-        assert_migrate_settings(
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::TreeSitter(
+                migrations::m_2025_10_01::SETTINGS_PATTERNS,
+                &SETTINGS_QUERY_2025_10_01,
+            )],
             &r#"{
                 "formatter": {
                     "code_actions": {
@@ -1500,7 +1509,11 @@ mod tests {
     #[test]
     fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
      {
-        assert_migrate_settings(
+        assert_migrate_settings_with_migrations(
+            &[MigrationType::TreeSitter(
+                migrations::m_2025_10_01::SETTINGS_PATTERNS,
+                &SETTINGS_QUERY_2025_10_01,
+            )],
             &r#"{
                 "formatter": {
                     "code_actions": {
@@ -1916,297 +1929,91 @@ mod tests {
     }
 
     #[test]
-    fn test_code_actions_on_format_migration_basic() {
+    fn test_restore_code_actions_on_format() {
         assert_migrate_settings_with_migrations(
             &[MigrationType::Json(
-                migrations::m_2025_10_10::remove_code_actions_on_format,
+                migrations::m_2025_10_16::restore_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
+                "formatter": {
+                    "code_action": "foo"
                 }
             }"#
             .unindent(),
             Some(
                 &r#"{
-                    "formatter": [
-                        {
-                            "code_action": "a"
-                        },
-                        {
-                            "code_action": "c"
-                        }
-                    ]
+                    "code_actions_on_format": {
+                        "foo": true
+                    }
                 }
                 "#
                 .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,
+                migrations::m_2025_10_16::restore_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
-              }
+                "formatter": [
+                    { "code_action": "foo" },
+                    "auto"
+                ]
             }"#
             .unindent(),
-            Some(
-                &r#"{
-                  "formatter": [
-                    {
-                      "code_action": "source.organizeImports"
-                    },
-                    {
-                      "code_action": "source.fixAll"
-                    },
-                    "prettier",
-                    {
-                      "language_server": "eslint"
-                    }
-                  ]
-                }"#
-                .unindent(),
-            ),
+            None,
         );
-    }
 
-    #[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,
+                migrations::m_2025_10_16::restore_code_actions_on_format,
             )],
             &r#"{
-                "languages": {
-                    "JavaScript": {
-                        "code_actions_on_format": {
-                            "source.fixAll.eslint": true
-                        }
-                    },
-                    "Go": {
-                        "code_actions_on_format": {
-                            "source.organizeImports": true
-                        }
-                    }
+                "formatter": {
+                    "code_action": "foo"
+                },
+                "code_actions_on_format": {
+                    "bar": true,
+                    "baz": false
                 }
             }"#
             .unindent(),
             Some(
                 &r#"{
-                    "languages": {
-                        "JavaScript": {
-                            "formatter": [
-                                {
-                                    "code_action": "source.fixAll.eslint"
-                                }
-                            ]
-                        },
-                        "Go": {
-                            "formatter": [
-                                {
-                                    "code_action": "source.organizeImports"
-                                }
-                            ]
-                        }
+                    "code_actions_on_format": {
+                        "foo": true,
+                        "bar": true,
+                        "baz": false
                     }
                 }"#
                 .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,
+                migrations::m_2025_10_16::restore_code_actions_on_format,
             )],
             &r#"{
-              "languages": {
-                "JavaScript": {
-                  "formatter": "prettier",
-                  "code_actions_on_format": {
-                    "source.fixAll.eslint": true,
-                    "source.organizeImports": false
-                  }
+                "formatter": [
+                    { "code_action": "foo" },
+                    { "code_action": "qux" },
+                ],
+                "code_actions_on_format": {
+                    "bar": true,
+                    "baz": false
                 }
-              }
             }"#
             .unindent(),
             Some(
                 &r#"{
-                  "languages": {
-                    "JavaScript": {
-                      "formatter": [
-                        {
-                          "code_action": "source.fixAll.eslint"
-                        },
-                        "prettier"
-                      ]
+                    "code_actions_on_format": {
+                        "foo": true,
+                        "qux": true,
+                        "bar": true,
+                        "baz": false
                     }
-                  }
-                }"#
-                .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/migrator/src/patterns.rs 🔗

@@ -10,4 +10,5 @@ pub(crate) use settings::{
     SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
     SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
     SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
+    migrate_language_setting,
 };

crates/migrator/src/patterns/settings.rs 🔗

@@ -108,3 +108,24 @@ pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
     (#eq? @agent1 "agent")
     (#eq? @agent2 "agent")
 )"#;
+
+/// Migrate language settings,
+/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key
+/// Fails early if `migrate_fn` returns an error at any point
+pub fn migrate_language_setting(
+    value: &mut serde_json::Value,
+    migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>,
+) -> anyhow::Result<()> {
+    migrate_fn(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];
+            migrate_fn(language, &path)?;
+        }
+    }
+    Ok(())
+}

crates/project/src/lsp_store.rs 🔗

@@ -1336,6 +1336,32 @@ 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) => {
@@ -1343,6 +1369,11 @@ impl LocalLspStore {
             }
         };
 
+        let formatters = code_actions_on_format_formatters
+            .iter()
+            .flatten()
+            .chain(formatters);
+
         for formatter in formatters {
             let formatter = if formatter == &Formatter::Auto {
                 if settings.prettier.allowed {

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

@@ -300,6 +300,11 @@ 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 🔗

@@ -5395,6 +5395,27 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
             metadata: None,
             files: USER | LOCAL,
         }),
+        SettingsPageItem::SettingItem(SettingItem {
+            title: "Code Actions On Format",
+            description: "Additional Code Actions To Run When Formatting",
+            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("Autoclose"),
         SettingsPageItem::SettingItem(SettingItem {
             title: "Use Autoclose",

docs/src/languages/javascript.md 🔗

@@ -92,10 +92,8 @@ the formatter:
 {
   "languages": {
     "JavaScript": {
-      "formatter": {
-        "code_actions": {
-          "source.fixAll.eslint": true
-        }
+      "code_actions_on_format": {
+        "source.fixAll.eslint": true
       }
     }
   }