settings: Make "auto" and "language_server" valid format steps (#40113)

Ben Kunkle created

Follow up for: #39983 and
https://github.com/zed-industries/zed/pull/40040#issuecomment-3393902691

Previously it was possible to have formatting done using prettier or
language server using `"formatter": "auto"` and specify code actions to
apply on format using the `"code_actions_on_format"` setting. However,
post #39983 this is no longer possible due to the removal of the
`"code_actions_on_format"` setting. To rectify this regression, this PR
makes it so that the `"auto"` and `"language_server"` strings that were
previously only allowed as top level values on the `"formatter"` key,
are now allowed as format steps like so:
```json
{
      "formatter": ["auto", "language_server"]
}
```

Therefore to replicate the previous behavior using `"auto"` and
`"code_actions_on_format"` you can use the following configuration:

```json
{
      "formatter": [{"code_action": ...}, "auto"]
}
```

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

assets/settings/default.json                                  |  22 
crates/agent2/src/tools/edit_file_tool.rs                     |   2 
crates/assistant_tools/src/edit_file_tool.rs                  |   2 
crates/collab/src/tests/integration_tests.rs                  |  17 
crates/collab/src/tests/remote_editing_collaboration_tests.rs |  12 
crates/editor/src/editor_tests.rs                             |  25 
crates/language/src/language_settings.rs                      |   4 
crates/project/src/lsp_store.rs                               |  54 
crates/project/src/prettier_store.rs                          |  13 
crates/settings/src/settings_content/language.rs              | 223 ++--
10 files changed, 183 insertions(+), 191 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1107,22 +1107,28 @@
   // 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:
+  // How to perform a buffer format. This setting can take multiple values:
   //
-  // 1. Format code using the current language server:
+  // 1. Default. Format files using Zed's Prettier integration (if applicable),
+  //    or falling back to formatting via language server:
+  //     "formatter": "auto"
+  // 2. Format code using the current language server:
   //     "formatter": "language_server"
-  // 2. Format code using an external command:
+  // 3. Format code using a specific language server:
+  //     "formatter": {"language_server": {"name": "ruff"}}
+  // 4. Format code using an external command:
   //     "formatter": {
   //       "external": {
   //         "command": "prettier",
   //         "arguments": ["--stdin-filepath", "{buffer_path}"]
   //       }
   //     }
-  // 3. Format code using Zed's Prettier integration:
+  // 5. Format code using Zed's Prettier integration:
   //     "formatter": "prettier"
-  // 4. Default. Format files using Zed's Prettier integration (if applicable),
-  //    or falling back to formatting via language server:
-  //     "formatter": "auto"
+  // 6. Format code using a code action
+  //     "formatter": {"code_action": "source.fixAll.eslint"}
+  // 7. An array of any format step specified above to apply in order
+  //     "formatter": [{"code_action": "source.fixAll.eslint"}, "prettier"]
   "formatter": "auto",
   // How to soft-wrap long lines of text.
   // Possible values:
@@ -1690,7 +1696,7 @@
       "preferred_line_length": 72
     },
     "Go": {
-      "formatter": [{ "code_action": "source.organizeImports" }, { "language_server": {} }],
+      "formatter": [{ "code_action": "source.organizeImports" }, "language_server"],
       "debuggers": ["Delve"]
     },
     "GraphQL": {

crates/agent2/src/tools/edit_file_tool.rs 🔗

@@ -790,7 +790,7 @@ mod tests {
                 store.update_user_settings(cx, |settings| {
                     settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
                     settings.project.all_languages.defaults.formatter =
-                        Some(language::language_settings::SelectedFormatter::Auto);
+                        Some(language::language_settings::FormatterList::default());
                 });
             });
         });

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -1538,7 +1538,7 @@ mod tests {
                 store.update_user_settings(cx, |settings| {
                     settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
                     settings.project.all_languages.defaults.formatter =
-                        Some(language::language_settings::SelectedFormatter::Auto);
+                        Some(language::language_settings::FormatterList::default());
                 });
             });
         });

crates/collab/src/tests/integration_tests.rs 🔗

@@ -25,7 +25,7 @@ use gpui::{
 use language::{
     Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
     LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
-    language_settings::{Formatter, FormatterList, SelectedFormatter},
+    language_settings::{Formatter, FormatterList},
     tree_sitter_rust, tree_sitter_typescript,
 };
 use lsp::{LanguageServerId, OneOf};
@@ -39,7 +39,7 @@ use project::{
 use prompt_store::PromptBuilder;
 use rand::prelude::*;
 use serde_json::json;
-use settings::{PrettierSettingsContent, SettingsStore};
+use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
 use std::{
     cell::{Cell, RefCell},
     env, future, mem,
@@ -4610,14 +4610,13 @@ async fn test_formatting_buffer(
         cx_a.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
                 store.update_user_settings(cx, |file| {
-                    file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
-                        FormatterList::Single(Formatter::External {
+                    file.project.all_languages.defaults.formatter =
+                        Some(FormatterList::Single(Formatter::External {
                             command: "awk".into(),
                             arguments: Some(
                                 vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
                             ),
-                        }),
-                    ));
+                        }));
                 });
             });
         });
@@ -4708,7 +4707,7 @@ async fn test_prettier_formatting_buffer(
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings(cx, |file| {
-                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
+                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
                 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
                     allowed: Some(true),
                     ..Default::default()
@@ -4719,8 +4718,8 @@ async fn test_prettier_formatting_buffer(
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings(cx, |file| {
-                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
-                    FormatterList::Single(Formatter::LanguageServer { name: None }),
+                file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
+                    Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
                 ));
                 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
                     allowed: Some(true),

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -14,7 +14,7 @@ use gpui::{
 use http_client::BlockedHttpClient;
 use language::{
     FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
-    language_settings::{Formatter, FormatterList, SelectedFormatter, language_settings},
+    language_settings::{Formatter, FormatterList, language_settings},
     tree_sitter_typescript,
 };
 use node_runtime::NodeRuntime;
@@ -27,7 +27,7 @@ use remote::RemoteClient;
 use remote_server::{HeadlessAppState, HeadlessProject};
 use rpc::proto;
 use serde_json::json;
-use settings::{PrettierSettingsContent, SettingsStore};
+use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
 use std::{
     path::Path,
     sync::{Arc, atomic::AtomicUsize},
@@ -491,7 +491,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings(cx, |file| {
-                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
+                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
                 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
                     allowed: Some(true),
                     ..Default::default()
@@ -502,8 +502,8 @@ async fn test_ssh_collaboration_formatting_with_prettier(
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings(cx, |file| {
-                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
-                    FormatterList::Single(Formatter::LanguageServer { name: None }),
+                file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
+                    Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
                 ));
                 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
                     allowed: Some(true),
@@ -550,7 +550,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings(cx, |file| {
-                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
+                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
                 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
                     allowed: Some(true),
                     ..Default::default()

crates/editor/src/editor_tests.rs 🔗

@@ -27,7 +27,6 @@ use language::{
     LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
     language_settings::{
         CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
-        SelectedFormatter,
     },
     tree_sitter_python,
 };
@@ -11908,8 +11907,8 @@ async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppC
 #[gpui::test]
 async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
     init_test(cx, |settings| {
-        settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
-            Formatter::LanguageServer { name: None },
+        settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
+            settings::LanguageServerFormatterSpecifier::Current,
         )))
     });
 
@@ -12034,11 +12033,11 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
 async fn test_multiple_formatters(cx: &mut TestAppContext) {
     init_test(cx, |settings| {
         settings.defaults.remove_trailing_whitespace_on_save = Some(true);
-        settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
-            Formatter::LanguageServer { name: None },
+        settings.defaults.formatter = Some(FormatterList::Vec(vec![
+            Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
             Formatter::CodeAction("code-action-1".into()),
             Formatter::CodeAction("code-action-2".into()),
-        ])))
+        ]))
     });
 
     let fs = FakeFs::new(cx.executor());
@@ -12293,9 +12292,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
     init_test(cx, |settings| {
-        settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
-            Formatter::LanguageServer { name: None },
-        ])))
+        settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
+            settings::LanguageServerFormatterSpecifier::Current,
+        )]))
     });
 
     let fs = FakeFs::new(cx.executor());
@@ -12498,7 +12497,7 @@ async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
     init_test(cx, |settings| {
-        settings.defaults.formatter = Some(SelectedFormatter::Auto)
+        settings.defaults.formatter = Some(FormatterList::default())
     });
 
     let mut cx = EditorLspTestContext::new_rust(
@@ -18187,9 +18186,7 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
 #[gpui::test]
 async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
     init_test(cx, |settings| {
-        settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
-            Formatter::Prettier,
-        )))
+        settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
     });
 
     let fs = FakeFs::new(cx.executor());
@@ -18256,7 +18253,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
     );
 
     update_test_language_settings(cx, |settings| {
-        settings.defaults.formatter = Some(SelectedFormatter::Auto)
+        settings.defaults.formatter = Some(FormatterList::default())
     });
     let format = editor.update_in(cx, |editor, window, cx| {
         editor.perform_format(

crates/language/src/language_settings.rs 🔗

@@ -13,7 +13,7 @@ use itertools::{Either, Itertools};
 pub use settings::{
     CompletionSettingsContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave,
     Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LspInsertMode,
-    RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
+    RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
 };
 use settings::{ExtendingVec, Settings, SettingsContent, SettingsLocation, SettingsStore};
 use shellexpand;
@@ -96,7 +96,7 @@ pub struct LanguageSettings {
     /// when saving it.
     pub ensure_final_newline_on_save: bool,
     /// How to perform a buffer format.
-    pub formatter: settings::SelectedFormatter,
+    pub formatter: settings::FormatterList,
     /// Zed's Prettier integration settings.
     pub prettier: PrettierSettings,
     /// Whether to automatically close JSX tags.

crates/project/src/lsp_store.rs 🔗

@@ -61,9 +61,7 @@ use language::{
     LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, ManifestDelegate,
     ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain,
     Transaction, Unclipped,
-    language_settings::{
-        FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
-    },
+    language_settings::{FormatOnSave, Formatter, LanguageSettings, language_settings},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_lsp_edit, deserialize_version, serialize_anchor,
@@ -1338,23 +1336,24 @@ impl LocalLspStore {
         let formatters = match (trigger, &settings.format_on_save) {
             (FormatTrigger::Save, FormatOnSave::Off) => &[],
             (FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
-                match &settings.formatter {
-                    SelectedFormatter::Auto => {
-                        if settings.prettier.allowed {
-                            zlog::trace!(logger => "Formatter set to auto: defaulting to prettier");
-                            std::slice::from_ref(&Formatter::Prettier)
-                        } else {
-                            zlog::trace!(logger => "Formatter set to auto: defaulting to primary language server");
-                            std::slice::from_ref(&Formatter::LanguageServer { name: None })
-                        }
-                    }
-                    SelectedFormatter::List(formatter_list) => formatter_list.as_ref(),
-                }
+                settings.formatter.as_ref()
             }
         };
 
         for formatter in formatters {
+            let formatter = if formatter == &Formatter::Auto {
+                if settings.prettier.allowed {
+                    zlog::trace!(logger => "Formatter set to auto: defaulting to prettier");
+                    &Formatter::Prettier
+                } else {
+                    zlog::trace!(logger => "Formatter set to auto: defaulting to primary language server");
+                    &Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current)
+                }
+            } else {
+                formatter
+            };
             match formatter {
+                Formatter::Auto => unreachable!("Auto resolved above"),
                 Formatter::Prettier => {
                     let logger = zlog::scoped!(logger => "prettier");
                     zlog::trace!(logger => "formatting");
@@ -1409,7 +1408,7 @@ impl LocalLspStore {
                         },
                     )?;
                 }
-                Formatter::LanguageServer { name } => {
+                Formatter::LanguageServer(specifier) => {
                     let logger = zlog::scoped!(logger => "language-server");
                     zlog::trace!(logger => "formatting");
                     let _timer = zlog::time!(logger => "Formatting buffer using language server");
@@ -1419,16 +1418,19 @@ impl LocalLspStore {
                         continue;
                     };
 
-                    let language_server = if let Some(name) = name.as_deref() {
-                        adapters_and_servers.iter().find_map(|(adapter, server)| {
-                            if adapter.name.0.as_ref() == name {
-                                Some(server.clone())
-                            } else {
-                                None
-                            }
-                        })
-                    } else {
-                        adapters_and_servers.first().map(|e| e.1.clone())
+                    let language_server = match specifier {
+                        settings::LanguageServerFormatterSpecifier::Specific { name } => {
+                            adapters_and_servers.iter().find_map(|(adapter, server)| {
+                                if adapter.name.0.as_ref() == name {
+                                    Some(server.clone())
+                                } else {
+                                    None
+                                }
+                            })
+                        }
+                        settings::LanguageServerFormatterSpecifier::Current => {
+                            adapters_and_servers.first().map(|e| e.1.clone())
+                        }
                     };
 
                     let Some(language_server) = language_server else {

crates/project/src/prettier_store.rs 🔗

@@ -16,7 +16,7 @@ use futures::{
 use gpui::{AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
 use language::{
     Buffer, LanguageRegistry, LocalFile,
-    language_settings::{Formatter, LanguageSettings, SelectedFormatter},
+    language_settings::{Formatter, LanguageSettings},
 };
 use lsp::{LanguageServer, LanguageServerId, LanguageServerName};
 use node_runtime::NodeRuntime;
@@ -700,14 +700,11 @@ impl PrettierStore {
 pub fn prettier_plugins_for_language(
     language_settings: &LanguageSettings,
 ) -> Option<&HashSet<String>> {
-    match &language_settings.formatter {
-        SelectedFormatter::Auto => Some(&language_settings.prettier.plugins),
-
-        SelectedFormatter::List(list) => list
-            .as_ref()
-            .contains(&Formatter::Prettier)
-            .then_some(&language_settings.prettier.plugins),
+    let formatters = language_settings.formatter.as_ref();
+    if formatters.contains(&Formatter::Prettier) || formatters.contains(&Formatter::Auto) {
+        return Some(&language_settings.prettier.plugins);
     }
+    None
 }
 
 pub(super) async fn format_with_prettier(

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

@@ -1,12 +1,9 @@
-use std::{borrow::Cow, num::NonZeroU32};
+use std::num::NonZeroU32;
 
 use collections::{HashMap, HashSet};
 use gpui::{Modifiers, SharedString};
-use schemars::{JsonSchema, json_schema};
-use serde::{
-    Deserialize, Deserializer, Serialize,
-    de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
-};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 use serde_with::skip_serializing_none;
 use settings_macros::MergeFrom;
 use std::sync::Arc;
@@ -246,7 +243,7 @@ pub struct LanguageSettingsContent {
     /// How to perform a buffer format.
     ///
     /// Default: auto
-    pub formatter: Option<SelectedFormatter>,
+    pub formatter: Option<FormatterList>,
     /// Zed's Prettier integration settings.
     /// Allows to enable/disable formatting with Prettier
     /// and configure default Prettier, used when no project-level Prettier installation is found.
@@ -639,102 +636,6 @@ pub enum FormatOnSave {
     Off,
 }
 
-/// Controls which formatter should be used when formatting code.
-#[derive(Clone, Debug, Default, PartialEq, Eq, MergeFrom)]
-pub enum SelectedFormatter {
-    /// Format files using Zed's Prettier integration (if applicable),
-    /// or falling back to formatting via language server.
-    #[default]
-    Auto,
-    List(FormatterList),
-}
-
-impl JsonSchema for SelectedFormatter {
-    fn schema_name() -> Cow<'static, str> {
-        "Formatter".into()
-    }
-
-    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
-        let formatter_schema = Formatter::json_schema(generator);
-
-        json_schema!({
-            "oneOf": [
-                {
-                    "type": "array",
-                    "items": formatter_schema
-                },
-                {
-                    "type": "string",
-                    "enum": ["auto", "language_server"]
-                },
-                formatter_schema
-            ]
-        })
-    }
-}
-
-impl Serialize for SelectedFormatter {
-    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        match self {
-            SelectedFormatter::Auto => serializer.serialize_str("auto"),
-            SelectedFormatter::List(list) => list.serialize(serializer),
-        }
-    }
-}
-
-impl<'de> Deserialize<'de> for SelectedFormatter {
-    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        struct FormatDeserializer;
-
-        impl<'d> Visitor<'d> for FormatDeserializer {
-            type Value = SelectedFormatter;
-
-            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                formatter.write_str("a valid formatter kind")
-            }
-            fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
-            where
-                E: serde::de::Error,
-            {
-                if v == "auto" {
-                    Ok(Self::Value::Auto)
-                } else if v == "language_server" {
-                    Ok(Self::Value::List(FormatterList::Single(
-                        Formatter::LanguageServer { name: None },
-                    )))
-                } else {
-                    let ret: Result<FormatterList, _> =
-                        Deserialize::deserialize(v.into_deserializer());
-                    ret.map(SelectedFormatter::List)
-                }
-            }
-            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
-            where
-                A: MapAccess<'d>,
-            {
-                let ret: Result<FormatterList, _> =
-                    Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
-                ret.map(SelectedFormatter::List)
-            }
-            fn visit_seq<A>(self, map: A) -> Result<Self::Value, A::Error>
-            where
-                A: SeqAccess<'d>,
-            {
-                let ret: Result<FormatterList, _> =
-                    Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
-                ret.map(SelectedFormatter::List)
-            }
-        }
-        deserializer.deserialize_any(FormatDeserializer)
-    }
-}
-
 /// Controls which formatters should be used when formatting code.
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(untagged)]
@@ -762,10 +663,11 @@ impl AsRef<[Formatter]> for FormatterList {
 #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub enum Formatter {
-    /// Format code using the current language server.
-    LanguageServer { name: Option<String> },
-    /// Format code using Zed's Prettier integration.
+    /// Format files using Zed's Prettier integration (if applicable),
+    /// or falling back to formatting via language server.
     #[default]
+    Auto,
+    /// Format code using Zed's Prettier integration.
     Prettier,
     /// Format code using an external command.
     External {
@@ -776,6 +678,73 @@ pub enum Formatter {
     },
     /// Files should be formatted using a code action executed by language servers.
     CodeAction(String),
+    /// Format code using a language server.
+    #[serde(untagged)]
+    LanguageServer(LanguageServerFormatterSpecifier),
+}
+
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
+#[serde(
+    rename_all = "snake_case",
+    // allow specifying language servers as "language_server" or {"language_server": {"name": ...}}
+    from = "LanguageServerVariantContent",
+    into = "LanguageServerVariantContent"
+)]
+pub enum LanguageServerFormatterSpecifier {
+    Specific {
+        name: String,
+    },
+    #[default]
+    Current,
+}
+
+impl From<LanguageServerVariantContent> for LanguageServerFormatterSpecifier {
+    fn from(value: LanguageServerVariantContent) -> Self {
+        match value {
+            LanguageServerVariantContent::Specific {
+                language_server: LanguageServerSpecifierContent { name: Some(name) },
+            } => Self::Specific { name },
+            _ => Self::Current,
+        }
+    }
+}
+
+impl From<LanguageServerFormatterSpecifier> for LanguageServerVariantContent {
+    fn from(value: LanguageServerFormatterSpecifier) -> Self {
+        match value {
+            LanguageServerFormatterSpecifier::Specific { name } => Self::Specific {
+                language_server: LanguageServerSpecifierContent { name: Some(name) },
+            },
+            LanguageServerFormatterSpecifier::Current => {
+                Self::Current(CurrentLanguageServerContent::LanguageServer)
+            }
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
+#[serde(rename_all = "snake_case", untagged)]
+enum LanguageServerVariantContent {
+    /// Format code using a specific language server.
+    Specific {
+        language_server: LanguageServerSpecifierContent,
+    },
+    /// Format code using the current language server.
+    Current(CurrentLanguageServerContent),
+}
+
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
+#[serde(rename_all = "snake_case")]
+enum CurrentLanguageServerContent {
+    #[default]
+    LanguageServer,
+}
+
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
+#[serde(rename_all = "snake_case")]
+struct LanguageServerSpecifierContent {
+    /// The name of the language server to format with
+    name: Option<String>,
 }
 
 /// The settings for indent guides.
@@ -884,31 +853,53 @@ mod test {
     fn test_formatter_deserialization() {
         let raw_auto = "{\"formatter\": \"auto\"}";
         let settings: LanguageSettingsContent = serde_json::from_str(raw_auto).unwrap();
-        assert_eq!(settings.formatter, Some(SelectedFormatter::Auto));
+        assert_eq!(
+            settings.formatter,
+            Some(FormatterList::Single(Formatter::Auto))
+        );
         let raw = "{\"formatter\": \"language_server\"}";
         let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
         assert_eq!(
             settings.formatter,
-            Some(SelectedFormatter::List(FormatterList::Single(
-                Formatter::LanguageServer { name: None }
+            Some(FormatterList::Single(Formatter::LanguageServer(
+                LanguageServerFormatterSpecifier::Current
             )))
         );
+
         let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}";
         let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
         assert_eq!(
             settings.formatter,
-            Some(SelectedFormatter::List(FormatterList::Vec(vec![
-                Formatter::LanguageServer { name: None }
-            ])))
+            Some(FormatterList::Vec(vec![Formatter::LanguageServer(
+                LanguageServerFormatterSpecifier::Current
+            )]))
         );
-        let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}";
+        let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"language_server\", \"prettier\"]}";
         let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
         assert_eq!(
             settings.formatter,
-            Some(SelectedFormatter::List(FormatterList::Vec(vec![
-                Formatter::LanguageServer { name: None },
+            Some(FormatterList::Vec(vec![
+                Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
+                Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
                 Formatter::Prettier
-            ])))
+            ]))
+        );
+
+        let raw = "{\"formatter\": [{\"language_server\": {\"name\": \"ruff\"}}, \"prettier\"]}";
+        let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
+        assert_eq!(
+            settings.formatter,
+            Some(FormatterList::Vec(vec![
+                Formatter::LanguageServer(LanguageServerFormatterSpecifier::Specific {
+                    name: "ruff".to_string()
+                }),
+                Formatter::Prettier
+            ]))
+        );
+
+        assert_eq!(
+            serde_json::to_string(&LanguageServerFormatterSpecifier::Current).unwrap(),
+            "\"language_server\"",
         );
     }