From 2b301b02ceee9a24378a398011e364210b71bf58 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 10 Oct 2025 18:01:07 -0500 Subject: [PATCH] Deprecate code actions on format setting (#39983) 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 --- assets/settings/default.json | 7 +- crates/language/src/language_settings.rs | 3 - crates/migrator/src/migrations.rs | 6 + .../src/migrations/m_2025_10_10/settings.rs | 70 ++++ crates/migrator/src/migrator.rs | 299 ++++++++++++++++++ crates/project/src/lsp_store.rs | 31 -- .../settings/src/settings_content/language.rs | 5 - crates/settings_ui/src/page_data.rs | 21 -- 8 files changed, 377 insertions(+), 65 deletions(-) create mode 100644 crates/migrator/src/migrations/m_2025_10_10/settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 31aefdded314981a8bfe066ce941fc6ada7355ba..7c19036ae4dc53c4c30d658ed28b68eb051507b9 100644 --- a/assets/settings/default.json +++ b/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": { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index fba5888e16b493b0c587bef42899b179317c3d9b..63c3f9f8f71b9cb15a6e7cb04f4b2e901fdfcf5f 100644 --- a/crates/language/src/language_settings.rs +++ b/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, /// 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(), diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index ca125ff5e8726ce9e63ab11bab2643e9ce54378d..1b8ede68b1eb8686325c896723d1fdc762d02b73 100644 --- a/crates/migrator/src/migrations.rs +++ b/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; +} diff --git a/crates/migrator/src/migrations/m_2025_10_10/settings.rs b/crates/migrator/src/migrations/m_2025_10_10/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..1d07be71a139b60e4b362d26c68b25922f04a233 --- /dev/null +++ b/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"#, + 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(()) +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index dbf7605e1db3d56fbf3347e95b53c42f3abc081a..aea11f98c460cc6c72120138ca1068be9ea60923 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -213,6 +213,7 @@ pub fn migrate_settings(text: &str) -> Result> { 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(), + ), + ); + } } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 998deb197fa0ac622da50f3e9969f4774921f63e..2644f4f059c182eb31ca36e8d5f983a5302f5115 100644 --- a/crates/project/src/lsp_store.rs +++ b/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::>(), - ); - } - } - 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 => { diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index b56e64465336dbc7726c20ed187fe9e71068cb65..ca2a4e44620b85d4eddf25c6b74d0258008f0ae5 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -304,11 +304,6 @@ pub struct LanguageSettingsContent { /// /// Default: true pub use_on_type_format: Option, - /// 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>, /// Whether to perform linked edits of associated ranges, if the language server supports it. /// For example, when editing opening tag, the contents of the closing tag will be edited as well. /// diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index a447146ce13fcc37f7efd16bf54e3166d47247e8..afedde65962ed21f57277d5d9be95ff67b83ad4d 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -4840,27 +4840,6 @@ fn language_settings_data() -> Vec { 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",