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
This commit is contained in:
Ben Kunkle 2025-10-10 18:01:07 -05:00 committed by GitHub
parent f7e7a304e0
commit 3ba4b84107
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 377 additions and 65 deletions

View file

@ -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:
@ -1518,7 +1518,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": {},
@ -1688,9 +1687,7 @@
"preferred_line_length": 72
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
},
"formatter": [{ "code_action": "source.organizeImports" }, { "language_server": {} }],
"debuggers": ["Delve"]
},
"GraphQL": {

View file

@ -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.
@ -578,7 +576,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(),

View file

@ -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;
}

View file

@ -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(())
}

View file

@ -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(),
),
);
}
}

View file

@ -1335,32 +1335,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) => {
@ -1379,11 +1353,6 @@ impl LocalLspStore {
}
};
let formatters = code_actions_on_format_formatters
.iter()
.flatten()
.chain(formatters);
for formatter in formatters {
match formatter {
Formatter::Prettier => {

View file

@ -322,11 +322,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.
///

View file

@ -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",