Allow user to use multiple formatters (#14846)

Piotr Osiewicz and Thorsten created

Fixes #4822
- [x] Release note
- [ ] Surface formatting errors via a toast
- [x] Doc updates
- [x] Have "language-server" accept an optional name of the server.

Release Notes:

- `format` and `format_on_save` now accept an array of formatting
actions to run.
- `language_server` formatter option now accepts the name of a language
server to use (e.g. `{"language_server": {"name: "ruff"}}`); when not
specified, a primary language server is used.

---------

Co-authored-by: Thorsten <thorsten@zed.dev>

Change summary

Cargo.toml                                   |   2 
crates/collab/src/tests/integration_tests.rs |  21 
crates/editor/src/editor_tests.rs            |  14 
crates/language/src/language_settings.rs     | 289 +++++++++++++++++-
crates/project/src/prettier_support.rs       |  10 
crates/project/src/project.rs                | 344 ++++++++++++++++-----
docs/src/configuring-zed.md                  |  15 
7 files changed, 565 insertions(+), 130 deletions(-)

Detailed changes

Cargo.toml 🔗

@@ -364,7 +364,7 @@ runtimelib = { version = "0.12", default-features = false, features = [
 ] }
 rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
 rust-embed = { version = "8.4", features = ["include-exclude"] }
-schemars = "0.8"
+schemars = {version = "0.8", features = ["impl_json_schema"]}
 semver = "1.0"
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }

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

@@ -18,7 +18,9 @@ use gpui::{
     TestAppContext, UpdateGlobal,
 };
 use language::{
-    language_settings::{AllLanguageSettings, Formatter, PrettierSettings},
+    language_settings::{
+        AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
+    },
     tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
     LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
 };
@@ -4409,10 +4411,13 @@ async fn test_formatting_buffer(
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(Formatter::External {
-                    command: "awk".into(),
-                    arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
-                });
+                file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
+                    vec![Formatter::External {
+                        command: "awk".into(),
+                        arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
+                    }]
+                    .into(),
+                )));
             });
         });
     });
@@ -4493,7 +4498,7 @@ async fn test_prettier_formatting_buffer(
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(Formatter::Auto);
+                file.defaults.formatter = Some(SelectedFormatter::Auto);
                 file.defaults.prettier = Some(PrettierSettings {
                     allowed: true,
                     ..PrettierSettings::default()
@@ -4504,7 +4509,9 @@ async fn test_prettier_formatting_buffer(
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(Formatter::LanguageServer);
+                file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
+                    vec![Formatter::LanguageServer { name: None }].into(),
+                )));
                 file.defaults.prettier = Some(PrettierSettings {
                     allowed: true,
                     ..PrettierSettings::default()

crates/editor/src/editor_tests.rs 🔗

@@ -23,7 +23,7 @@ use language::{
     FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
     ParsedMarkdown, Point,
 };
-use language_settings::IndentGuideSettings;
+use language_settings::{Formatter, FormatterList, IndentGuideSettings};
 use multi_buffer::MultiBufferIndentGuide;
 use parking_lot::Mutex;
 use project::FakeFs;
@@ -6559,7 +6559,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
 #[gpui::test]
 async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
     init_test(cx, |settings| {
-        settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
+        settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
+            FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
+        ))
     });
 
     let fs = FakeFs::new(cx.executor());
@@ -6720,7 +6722,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
 #[gpui::test]
 async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
     init_test(cx, |settings| {
-        settings.defaults.formatter = Some(language_settings::Formatter::Auto)
+        settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
     });
 
     let mut cx = EditorLspTestContext::new_rust(
@@ -9723,7 +9725,9 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
 #[gpui::test]
 async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     init_test(cx, |settings| {
-        settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
+        settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
+            FormatterList(vec![Formatter::Prettier].into()),
+        ))
     });
 
     let fs = FakeFs::new(cx.executor());
@@ -9783,7 +9787,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     );
 
     update_test_language_settings(cx, |settings| {
-        settings.defaults.formatter = Some(language_settings::Formatter::Auto)
+        settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
     });
     let format = editor.update(cx, |editor, cx| {
         editor.perform_format(project.clone(), FormatTrigger::Manual, cx)

crates/language/src/language_settings.rs 🔗

@@ -3,14 +3,19 @@
 use crate::{File, Language, LanguageServerName};
 use anyhow::Result;
 use collections::{HashMap, HashSet};
+use core::slice;
 use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
 use gpui::AppContext;
 use itertools::{Either, Itertools};
 use schemars::{
-    schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
+    schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
     JsonSchema,
 };
-use serde::{Deserialize, Serialize};
+use serde::{
+    de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
+    Deserialize, Deserializer, Serialize,
+};
+use serde_json::Value;
 use settings::{Settings, SettingsLocation, SettingsSources};
 use std::{num::NonZeroU32, path::Path, sync::Arc};
 use util::serde::default_true;
@@ -89,7 +94,7 @@ pub struct LanguageSettings {
     /// when saving it.
     pub ensure_final_newline_on_save: bool,
     /// How to perform a buffer format.
-    pub formatter: Formatter,
+    pub formatter: SelectedFormatter,
     /// Zed's Prettier integration settings.
     pub prettier: PrettierSettings,
     /// Whether to use language servers to provide code intelligence.
@@ -274,7 +279,7 @@ pub struct LanguageSettingsContent {
     ///
     /// Default: auto
     #[serde(default)]
-    pub formatter: Option<Formatter>,
+    pub formatter: Option<SelectedFormatter>,
     /// 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.
@@ -381,24 +386,115 @@ pub enum SoftWrap {
 }
 
 /// Controls the behavior of formatting files when they are saved.
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum FormatOnSave {
     /// Files should be formatted on save.
     On,
     /// Files should not be formatted on save.
     Off,
-    /// Files should be formatted using the current language server.
-    LanguageServer,
-    /// The external program to use to format the files on save.
-    External {
-        /// The external program to run.
-        command: Arc<str>,
-        /// The arguments to pass to the program.
-        arguments: Arc<[String]>,
-    },
-    /// Files should be formatted using code actions executed by language servers.
-    CodeActions(HashMap<String, bool>),
+    List(FormatterList),
+}
+
+impl JsonSchema for FormatOnSave {
+    fn schema_name() -> String {
+        "OnSaveFormatter".into()
+    }
+
+    fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
+        let mut schema = SchemaObject::default();
+        let formatter_schema = Formatter::json_schema(generator);
+        schema.instance_type = Some(
+            vec![
+                InstanceType::Object,
+                InstanceType::String,
+                InstanceType::Array,
+            ]
+            .into(),
+        );
+
+        let mut valid_raw_values = SchemaObject::default();
+        valid_raw_values.enum_values = Some(vec![
+            Value::String("on".into()),
+            Value::String("off".into()),
+            Value::String("prettier".into()),
+            Value::String("language_server".into()),
+        ]);
+        let mut nested_values = SchemaObject::default();
+
+        nested_values.array().items = Some(formatter_schema.clone().into());
+
+        schema.subschemas().any_of = Some(vec![
+            nested_values.into(),
+            valid_raw_values.into(),
+            formatter_schema,
+        ]);
+        schema.into()
+    }
+}
+
+impl Serialize for FormatOnSave {
+    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        match self {
+            Self::On => serializer.serialize_str("on"),
+            Self::Off => serializer.serialize_str("off"),
+            Self::List(list) => list.serialize(serializer),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for FormatOnSave {
+    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 = FormatOnSave;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a valid on-save formatter kind")
+            }
+            fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                if v == "on" {
+                    Ok(Self::Value::On)
+                } else if v == "off" {
+                    Ok(Self::Value::Off)
+                } else if v == "language_server" {
+                    Ok(Self::Value::List(FormatterList(
+                        Formatter::LanguageServer { name: None }.into(),
+                    )))
+                } else {
+                    let ret: Result<FormatterList, _> =
+                        Deserialize::deserialize(v.into_deserializer());
+                    ret.map(Self::Value::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(Self::Value::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(Self::Value::List)
+            }
+        }
+        deserializer.deserialize_any(FormatDeserializer)
+    }
 }
 
 /// Controls how whitespace should be displayedin the editor.
@@ -421,15 +517,131 @@ pub enum ShowWhitespaceSetting {
 }
 
 /// Controls which formatter should be used when formatting code.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Formatter {
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+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() -> String {
+        "Formatter".into()
+    }
+
+    fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
+        let mut schema = SchemaObject::default();
+        let formatter_schema = Formatter::json_schema(generator);
+        schema.instance_type = Some(
+            vec![
+                InstanceType::Object,
+                InstanceType::String,
+                InstanceType::Array,
+            ]
+            .into(),
+        );
+
+        let mut valid_raw_values = SchemaObject::default();
+        valid_raw_values.enum_values = Some(vec![
+            Value::String("auto".into()),
+            Value::String("prettier".into()),
+            Value::String("language_server".into()),
+        ]);
+        let mut nested_values = SchemaObject::default();
+
+        nested_values.array().items = Some(formatter_schema.clone().into());
+
+        schema.subschemas().any_of = Some(vec![
+            nested_values.into(),
+            valid_raw_values.into(),
+            formatter_schema,
+        ]);
+        schema.into()
+    }
+}
+
+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(
+                        Formatter::LanguageServer { name: None }.into(),
+                    )))
+                } 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 formatter should be used when formatting code.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case", transparent)]
+pub struct FormatterList(pub SingleOrVec<Formatter>);
+
+impl AsRef<[Formatter]> for FormatterList {
+    fn as_ref(&self) -> &[Formatter] {
+        match &self.0 {
+            SingleOrVec::Single(single) => slice::from_ref(single),
+            SingleOrVec::Vec(v) => &v,
+        }
+    }
+}
+
+/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Formatter {
     /// Format code using the current language server.
-    LanguageServer,
+    LanguageServer { name: Option<String> },
     /// Format code using Zed's Prettier integration.
     Prettier,
     /// Format code using an external command.
@@ -898,6 +1110,41 @@ pub struct PrettierSettings {
 mod tests {
     use super::*;
 
+    #[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));
+        let raw = "{\"formatter\": \"language_server\"}";
+        let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
+        assert_eq!(
+            settings.formatter,
+            Some(SelectedFormatter::List(FormatterList(
+                Formatter::LanguageServer { name: None }.into()
+            )))
+        );
+        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![Formatter::LanguageServer { name: None }].into()
+            )))
+        );
+        let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}";
+        let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
+        assert_eq!(
+            settings.formatter,
+            Some(SelectedFormatter::List(FormatterList(
+                vec![
+                    Formatter::LanguageServer { name: None },
+                    Formatter::Prettier
+                ]
+                .into()
+            )))
+        );
+    }
+
     #[test]
     pub fn test_resolve_language_servers() {
         fn language_server_names(names: &[&str]) -> Vec<LanguageServerName> {

crates/project/src/prettier_support.rs 🔗

@@ -13,7 +13,7 @@ use futures::{
 };
 use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
 use language::{
-    language_settings::{Formatter, LanguageSettings},
+    language_settings::{Formatter, LanguageSettings, SelectedFormatter},
     Buffer, LanguageServerName, LocalFile,
 };
 use lsp::{LanguageServer, LanguageServerId};
@@ -30,8 +30,12 @@ pub fn prettier_plugins_for_language(
     language_settings: &LanguageSettings,
 ) -> Option<&HashSet<String>> {
     match &language_settings.formatter {
-        Formatter::Prettier { .. } | Formatter::Auto => Some(&language_settings.prettier.plugins),
-        Formatter::LanguageServer | Formatter::External { .. } | Formatter::CodeActions(_) => None,
+        SelectedFormatter::Auto => Some(&language_settings.prettier.plugins),
+
+        SelectedFormatter::List(list) => list
+            .as_ref()
+            .contains(&Formatter::Prettier)
+            .then_some(&language_settings.prettier.plugins),
     }
 }
 

crates/project/src/project.rs 🔗

@@ -43,7 +43,7 @@ use itertools::Itertools;
 use language::{
     language_settings::{
         language_settings, AllLanguageSettings, FormatOnSave, Formatter, InlayHintKind,
-        LanguageSettings,
+        LanguageSettings, SelectedFormatter,
     },
     markdown, point_to_lsp, prepare_completion_documentation,
     proto::{
@@ -5056,107 +5056,180 @@ impl Project {
                 .as_ref()
                 .zip(buffer_abs_path.as_ref());
 
-            let mut format_operation = None;
             let prettier_settings = buffer.read_with(&mut cx, |buffer, cx| {
                 language_settings(buffer.language(), buffer.file(), cx)
                     .prettier
                     .clone()
             })?;
-            match (&settings.formatter, &settings.format_on_save) {
-                (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
-
-                (Formatter::CodeActions(code_actions), FormatOnSave::On | FormatOnSave::Off)
-                | (_, FormatOnSave::CodeActions(code_actions)) => {
-                    let code_actions = deserialize_code_actions(code_actions);
-                    if !code_actions.is_empty() {
-                        Self::execute_code_actions_on_servers(
-                            &project,
-                            &adapters_and_servers,
-                            code_actions,
-                            buffer,
-                            push_to_history,
-                            &mut project_transaction,
-                            &mut cx,
-                        )
-                        .await?;
-                    }
-                }
-                (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
-                | (_, FormatOnSave::LanguageServer) => {
-                    if let Some((language_server, buffer_abs_path)) = server_and_buffer {
-                        format_operation = Some(FormatOperation::Lsp(
-                            Self::format_via_lsp(
-                                &project,
-                                buffer,
-                                buffer_abs_path,
-                                language_server,
-                                &settings,
-                                &mut cx,
-                            )
-                            .await
-                            .context("failed to format via language server")?,
-                        ));
-                    }
-                }
 
-                (
-                    Formatter::External { command, arguments },
-                    FormatOnSave::On | FormatOnSave::Off,
-                )
-                | (_, FormatOnSave::External { command, arguments }) => {
-                    let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path());
-                    format_operation = Self::format_via_external_command(
-                        buffer,
-                        buffer_abs_path,
-                        command,
-                        arguments,
-                        &mut cx,
-                    )
-                    .await
-                    .context(format!(
-                        "failed to format via external command {:?}",
-                        command
-                    ))?
-                    .map(FormatOperation::External);
-                }
-                (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
-                    let prettier = if prettier_settings.allowed {
-                        prettier_support::format_with_prettier(&project, buffer, &mut cx)
-                            .await
-                            .transpose()
-                            .ok()
-                            .flatten()
-                    } else {
-                        None
-                    };
+            let mut format_operations: Vec<FormatOperation> = vec![];
+            {
+                match trigger {
+                    FormatTrigger::Save => {
+                        match &settings.format_on_save {
+                            FormatOnSave::Off => {
+                                // nothing
+                            }
+                            FormatOnSave::On => {
+                                match &settings.formatter {
+                                    SelectedFormatter::Auto => {
+                                        // do the auto-format: prefer prettier, fallback to primary language server
+                                        let diff = {
+                                            if prettier_settings.allowed {
+                                                Self::perform_format(
+                                                    &Formatter::Prettier,
+                                                    server_and_buffer,
+                                                    project.clone(),
+                                                    buffer,
+                                                    buffer_abs_path,
+                                                    &settings,
+                                                    &adapters_and_servers,
+                                                    push_to_history,
+                                                    &mut project_transaction,
+                                                    &mut cx,
+                                                )
+                                                .await
+                                            } else {
+                                                Self::perform_format(
+                                                    &Formatter::LanguageServer { name: None },
+                                                    server_and_buffer,
+                                                    project.clone(),
+                                                    buffer,
+                                                    buffer_abs_path,
+                                                    &settings,
+                                                    &adapters_and_servers,
+                                                    push_to_history,
+                                                    &mut project_transaction,
+                                                    &mut cx,
+                                                )
+                                                .await
+                                            }
+                                        }
+                                        .log_err()
+                                        .flatten();
+                                        if let Some(op) = diff {
+                                            format_operations.push(op);
+                                        }
+                                    }
+                                    SelectedFormatter::List(formatters) => {
+                                        for formatter in formatters.as_ref() {
+                                            let diff = Self::perform_format(
+                                                formatter,
+                                                server_and_buffer,
+                                                project.clone(),
+                                                buffer,
+                                                buffer_abs_path,
+                                                &settings,
+                                                &adapters_and_servers,
+                                                push_to_history,
+                                                &mut project_transaction,
+                                                &mut cx,
+                                            )
+                                            .await
+                                            .log_err()
+                                            .flatten();
+                                            if let Some(op) = diff {
+                                                format_operations.push(op);
+                                            }
 
-                    if let Some(operation) = prettier {
-                        format_operation = Some(operation);
-                    } else if let Some((language_server, buffer_abs_path)) = server_and_buffer {
-                        format_operation = Some(FormatOperation::Lsp(
-                            Self::format_via_lsp(
-                                &project,
-                                buffer,
-                                buffer_abs_path,
-                                language_server,
-                                &settings,
-                                &mut cx,
-                            )
-                            .await
-                            .context("failed to format via language server")?,
-                        ));
+                                            // format with formatter
+                                        }
+                                    }
+                                }
+                            }
+                            FormatOnSave::List(formatters) => {
+                                for formatter in formatters.as_ref() {
+                                    let diff = Self::perform_format(
+                                        &formatter,
+                                        server_and_buffer,
+                                        project.clone(),
+                                        buffer,
+                                        buffer_abs_path,
+                                        &settings,
+                                        &adapters_and_servers,
+                                        push_to_history,
+                                        &mut project_transaction,
+                                        &mut cx,
+                                    )
+                                    .await
+                                    .log_err()
+                                    .flatten();
+                                    if let Some(op) = diff {
+                                        format_operations.push(op);
+                                    }
+                                }
+                            }
+                        }
                     }
-                }
-                (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
-                    if prettier_settings.allowed {
-                        if let Some(operation) =
-                            prettier_support::format_with_prettier(&project, buffer, &mut cx).await
-                        {
-                            format_operation = Some(operation?);
+                    FormatTrigger::Manual => {
+                        match &settings.formatter {
+                            SelectedFormatter::Auto => {
+                                // do the auto-format: prefer prettier, fallback to primary language server
+                                let diff = {
+                                    if prettier_settings.allowed {
+                                        Self::perform_format(
+                                            &Formatter::Prettier,
+                                            server_and_buffer,
+                                            project.clone(),
+                                            buffer,
+                                            buffer_abs_path,
+                                            &settings,
+                                            &adapters_and_servers,
+                                            push_to_history,
+                                            &mut project_transaction,
+                                            &mut cx,
+                                        )
+                                        .await
+                                    } else {
+                                        Self::perform_format(
+                                            &Formatter::LanguageServer { name: None },
+                                            server_and_buffer,
+                                            project.clone(),
+                                            buffer,
+                                            buffer_abs_path,
+                                            &settings,
+                                            &adapters_and_servers,
+                                            push_to_history,
+                                            &mut project_transaction,
+                                            &mut cx,
+                                        )
+                                        .await
+                                    }
+                                }
+                                .log_err()
+                                .flatten();
+
+                                if let Some(op) = diff {
+                                    format_operations.push(op)
+                                }
+                            }
+                            SelectedFormatter::List(formatters) => {
+                                for formatter in formatters.as_ref() {
+                                    // format with formatter
+                                    let diff = Self::perform_format(
+                                        formatter,
+                                        server_and_buffer,
+                                        project.clone(),
+                                        buffer,
+                                        buffer_abs_path,
+                                        &settings,
+                                        &adapters_and_servers,
+                                        push_to_history,
+                                        &mut project_transaction,
+                                        &mut cx,
+                                    )
+                                    .await
+                                    .log_err()
+                                    .flatten();
+                                    if let Some(op) = diff {
+                                        format_operations.push(op);
+                                    }
+                                }
+                            }
                         }
                     }
                 }
-            };
+            }
 
             buffer.update(&mut cx, |b, cx| {
                 // If the buffer had its whitespace formatted and was edited while the language-specific
@@ -5166,13 +5239,13 @@ impl Project {
                     if b.peek_undo_stack()
                         .map_or(true, |e| e.transaction_id() != transaction_id)
                     {
-                        format_operation.take();
+                        format_operations.clear();
                     }
                 }
 
                 // Apply any language-specific formatting, and group the two formatting operations
                 // in the buffer's undo history.
-                if let Some(operation) = format_operation {
+                for operation in format_operations {
                     match operation {
                         FormatOperation::Lsp(edits) => {
                             b.edit(edits, None, cx);
@@ -5204,6 +5277,91 @@ impl Project {
         Ok(project_transaction)
     }
 
+    #[allow(clippy::too_many_arguments)]
+    async fn perform_format(
+        formatter: &Formatter,
+        primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
+        project: WeakModel<Project>,
+        buffer: &Model<Buffer>,
+        buffer_abs_path: &Option<PathBuf>,
+        settings: &LanguageSettings,
+        adapters_and_servers: &Vec<(Arc<CachedLspAdapter>, Arc<LanguageServer>)>,
+        push_to_history: bool,
+        transaction: &mut ProjectTransaction,
+        mut cx: &mut AsyncAppContext,
+    ) -> Result<Option<FormatOperation>, anyhow::Error> {
+        let result = match formatter {
+            Formatter::LanguageServer { name } => {
+                if let Some((language_server, buffer_abs_path)) = primary_server_and_buffer {
+                    let language_server = if let Some(name) = name {
+                        adapters_and_servers
+                            .iter()
+                            .find_map(|(adapter, server)| {
+                                adapter.name.0.as_ref().eq(name.as_str()).then_some(server)
+                            })
+                            .unwrap_or_else(|| language_server)
+                    } else {
+                        language_server
+                    };
+                    Some(FormatOperation::Lsp(
+                        Self::format_via_lsp(
+                            &project,
+                            buffer,
+                            buffer_abs_path,
+                            language_server,
+                            settings,
+                            cx,
+                        )
+                        .await
+                        .context("failed to format via language server")?,
+                    ))
+                } else {
+                    None
+                }
+            }
+            Formatter::Prettier => {
+                prettier_support::format_with_prettier(&project, buffer, &mut cx)
+                    .await
+                    .transpose()
+                    .ok()
+                    .flatten()
+            }
+            Formatter::External { command, arguments } => {
+                let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path());
+                Self::format_via_external_command(
+                    buffer,
+                    buffer_abs_path,
+                    &command,
+                    &arguments,
+                    &mut cx,
+                )
+                .await
+                .context(format!(
+                    "failed to format via external command {:?}",
+                    command
+                ))?
+                .map(FormatOperation::External)
+            }
+            Formatter::CodeActions(code_actions) => {
+                let code_actions = deserialize_code_actions(&code_actions);
+                if !code_actions.is_empty() {
+                    Self::execute_code_actions_on_servers(
+                        &project,
+                        &adapters_and_servers,
+                        code_actions,
+                        buffer,
+                        push_to_history,
+                        transaction,
+                        cx,
+                    )
+                    .await?;
+                }
+                None
+            }
+        };
+        anyhow::Ok(result)
+    }
+
     async fn format_via_lsp(
         this: &WeakModel<Self>,
         buffer: &Model<Buffer>,

docs/src/configuring-zed.md 🔗

@@ -601,6 +601,21 @@ To override settings for a language, add an entry for that language server's nam
 }
 ```
 
+4. Or to use multiple formatters consecutively, use an array of formatters:
+```json
+{
+  "formatter": [
+    {"language_server": {"name": "rust-analyzer"}},
+    {"external": {
+      "command": "sed",
+      "arguments": ["-e", "s/ *$//"]
+    }
+  ]
+}
+```
+Here `rust-analyzer` will be used first to format the code, followed by a call of sed.
+If any of the formatters fails, the subsequent ones will still be executed.
+
 ## Code Actions On Format
 
 - Description: The code actions to perform with the primary language server when formatting the buffer.