diff --git a/crates/migrator/src/migrations/m_2025_10_10/settings.rs b/crates/migrator/src/migrations/m_2025_10_10/settings.rs index 694f887b5962bb..c1b7aed8def9dc 100644 --- a/crates/migrator/src/migrations/m_2025_10_10/settings.rs +++ b/crates/migrator/src/migrations/m_2025_10_10/settings.rs @@ -1,18 +1,62 @@ use anyhow::Result; use serde_json::Value; -use crate::patterns::migrate_language_setting; - pub fn remove_code_actions_on_format(value: &mut Value) -> Result<()> { - migrate_language_setting(value, remove_code_actions_on_format_inner) + let defaults = + settings::parse_json_with_comments::(settings::default_settings().as_ref()).unwrap(); + let default_root_formatter = defaults.get("formatter"); + + let root_code_actions = + remove_code_actions_on_format_inner(value, default_root_formatter, &[], &[])?; + let user_default_formatter = value.get("formatter").cloned(); + + let user_languages = value + .as_object_mut() + .and_then(|obj| obj.get_mut("languages")) + .and_then(|languages| languages.as_object_mut()); + let default_languages = defaults + .as_object() + .and_then(|obj| obj.get("languages")) + .and_then(|languages| languages.as_object()); + + if let Some(languages) = user_languages { + for (language_name, language) in languages.iter_mut() { + let path = vec!["languages", language_name]; + let language_default_formatter = default_languages + .and_then(|langs| langs.get(language_name)) + .and_then(|lang| lang.get("formatter")); + + // override order: + // 4. root from defaults + // 3. language from defaults + // 2. root from user + // 1. language from user + let default_formatter_for_language = user_default_formatter + .as_ref() + .or(language_default_formatter) + .or(default_root_formatter); + remove_code_actions_on_format_inner( + language, + default_formatter_for_language, + &root_code_actions, + &path, + )?; + } + } + Ok(()) } -fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> { +fn remove_code_actions_on_format_inner( + value: &mut Value, + default_formatters: Option<&Value>, + parent_code_actions: &[String], + path: &[&str], +) -> Result> { let Some(obj) = value.as_object_mut() else { - return Ok(()); + return Ok(vec![]); }; let Some(code_actions_on_format) = obj.get("code_actions_on_format").cloned() else { - return Ok(()); + return Ok(vec![]); }; fn fmt_path(path: &[&str], key: &str) -> String { @@ -35,6 +79,7 @@ fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Resu } code_actions.push(code_action.clone()); } + code_actions.extend(parent_code_actions.iter().cloned()); let mut formatter_array = vec![]; if let Some(formatter) = obj.get("formatter") { @@ -43,12 +88,18 @@ fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Resu } else { formatter_array.insert(0, formatter.clone()); } + } else if let Some(formatter) = default_formatters { + if let Some(array) = formatter.as_array() { + formatter_array = array.clone(); + } else { + formatter_array.push(formatter.clone()); + } }; let found_code_actions = !code_actions.is_empty(); formatter_array.splice( 0..0, code_actions - .into_iter() + .iter() .map(|code_action| serde_json::json!({"code_action": code_action})), ); @@ -57,5 +108,5 @@ fn remove_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Resu obj.insert("formatter".to_string(), Value::Array(formatter_array)); } - Ok(()) + Ok(code_actions) } diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 7b4182b00509ef..7f41690a898c0c 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -2010,7 +2010,8 @@ mod tests { }, { "code_action": "source.fixAll" - } + }, + "auto" ] } "# @@ -2041,7 +2042,8 @@ mod tests { }, { "code_action": "c" - } + }, + "auto" ] } "# @@ -2127,7 +2129,7 @@ mod tests { "Go": { "code_actions_on_format": { "source.organizeImports": true - } + }, } } }"# @@ -2139,14 +2141,19 @@ mod tests { "formatter": [ { "code_action": "source.fixAll.eslint" - } + }, + "auto" ] }, "Go": { "formatter": [ { "code_action": "source.organizeImports" - } + }, + { + "code_action": "source.organizeImports" + }, + "language_server" ] } } @@ -2233,6 +2240,9 @@ mod tests { { "code_action": "source.organizeImports" }, + { + "code_action": "source.fixAll" + }, "rust-analyzer" ] }, @@ -2240,7 +2250,14 @@ mod tests { "formatter": [ { "code_action": "source.organizeImports" - } + }, + { + "code_action": "source.fixAll" + }, + { + "code_action": "source.fixAll" + }, + "prettier" ] } } @@ -2250,6 +2267,35 @@ mod tests { ); } + #[test] + fn test_code_actions_on_format_inserts_default_formatters() { + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_10_10::remove_code_actions_on_format, + )], + &r#"{ + "code_actions_on_format": { + "source.organizeImports": false, + "source.fixAll.eslint": true + } + }"# + .unindent(), + Some( + &r#" + { + "formatter": [ + { + "code_action": "source.fixAll.eslint" + }, + "auto" + ] + } + "# + .unindent(), + ), + ) + } + #[test] fn test_code_actions_on_format_no_migration_when_not_present() { assert_migrate_settings_with_migrations( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 68db3ac2b436e7..0d0188cfe809cc 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1710,12 +1710,7 @@ impl LocalLspStore { formatting_transaction_id, cx, |buffer, cx| { - zlog::info!( - "Applying edits {edits:?}. Content: {:?}", - buffer.text() - ); buffer.edit(edits, None, cx); - zlog::info!("Applied edits. New Content: {:?}", buffer.text()); }, )?; } diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md index 9a7157ceaecbcc..c8e257e3900a02 100644 --- a/docs/src/configuring-languages.md +++ b/docs/src/configuring-languages.md @@ -320,13 +320,18 @@ To run linter fixes automatically on save: ```json [settings] "languages": { "JavaScript": { - "formatter": { - "code_action": "source.fixAll.eslint" - } + "formatter": [ + { + "code_action": "source.fixAll.eslint" + }, + "auto" + ] } } ``` +This specifies that when a format is requested, Zed will first run the `source.fixAll.eslint` action, and then perform the actual format using the language-specific default formatter (for JavaScript this is Prettier, and for other languages it often corresponds to formatting using the default language server). + ### Integrating Formatting and Linting Zed allows you to run both formatting and linting on save. Here's an example that uses Prettier for formatting and ESLint for linting JavaScript files: diff --git a/docs/src/languages/javascript.md b/docs/src/languages/javascript.md index 6c7eff5f38977d..07a4cf6dfae7f2 100644 --- a/docs/src/languages/javascript.md +++ b/docs/src/languages/javascript.md @@ -49,9 +49,12 @@ You can configure Zed to format code using `eslint --fix` by running the ESLint { "languages": { "JavaScript": { - "formatter": { - "code_action": "source.fixAll.eslint" - } + "formatter": [ + { + "code_action": "source.fixAll.eslint" + }, + "auto" + ] } } } @@ -63,9 +66,12 @@ You can also only execute a single ESLint rule when using `fixAll`: { "languages": { "JavaScript": { - "formatter": { - "code_action": "source.fixAll.eslint" - } + "formatter": [ + { + "code_action": "source.fixAll.eslint" + }, + "auto" + ] } }, "lsp": {