Detailed changes
@@ -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": {
@@ -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(),
@@ -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;
}
@@ -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(())
-}
@@ -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(())
+}
@@ -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(),
),
@@ -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,
};
@@ -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(())
+}
@@ -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 {
@@ -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.
///
@@ -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",
@@ -92,10 +92,8 @@ the formatter:
{
"languages": {
"JavaScript": {
- "formatter": {
- "code_actions": {
- "source.fixAll.eslint": true
- }
+ "code_actions_on_format": {
+ "source.fixAll.eslint": true
}
}
}