From 3d1bf092998ac28887632c93b737727e7b65cc5e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 23 Jul 2024 20:05:09 +0200 Subject: [PATCH] Allow user to use multiple formatters (#14846) 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 --- 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(-) diff --git a/Cargo.toml b/Cargo.toml index 9e9500aef6..dd381a19f1 100644 --- a/Cargo.toml +++ b/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"] } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index f98cb5de0e..98cbfdca63 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/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::(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::(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::(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() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f77738f876..aa1bc27ce8 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index eacf8ac645..cbd4ed68c3 100644 --- a/crates/language/src/language_settings.rs +++ b/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, + pub formatter: Option, /// 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, - /// The arguments to pass to the program. - arguments: Arc<[String]>, - }, - /// Files should be formatted using code actions executed by language servers. - CodeActions(HashMap), + 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(&self, serializer: S) -> std::result::Result + 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(deserializer: D) -> std::result::Result + 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(self, v: &str) -> std::result::Result + 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 = + Deserialize::deserialize(v.into_deserializer()); + ret.map(Self::Value::List) + } + } + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'d>, + { + let ret: Result = + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); + ret.map(Self::Value::List) + } + fn visit_seq(self, map: A) -> Result + where + A: SeqAccess<'d>, + { + let ret: Result = + 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(&self, serializer: S) -> std::result::Result + 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(deserializer: D) -> std::result::Result + 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(self, v: &str) -> std::result::Result + 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 = + Deserialize::deserialize(v.into_deserializer()); + ret.map(SelectedFormatter::List) + } + } + fn visit_map(self, map: A) -> Result + where + A: MapAccess<'d>, + { + let ret: Result = + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); + ret.map(SelectedFormatter::List) + } + fn visit_seq(self, map: A) -> Result + where + A: SeqAccess<'d>, + { + let ret: Result = + 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); + +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 }, /// 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 { diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_support.rs index f94ba558b0..62c6cf26d7 100644 --- a/crates/project/src/prettier_support.rs +++ b/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> { 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), } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 54b38717f1..cf6e8b5ae7 100644 --- a/crates/project/src/project.rs +++ b/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")?, - )); - } - } + let mut format_operations: Vec = 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); + } - ( - 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 - }; - - 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, &PathBuf)>, + project: WeakModel, + buffer: &Model, + buffer_abs_path: &Option, + settings: &LanguageSettings, + adapters_and_servers: &Vec<(Arc, Arc)>, + push_to_history: bool, + transaction: &mut ProjectTransaction, + mut cx: &mut AsyncAppContext, + ) -> Result, 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, buffer: &Model, diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 5748cb90b4..c8a724c9db 100644 --- a/docs/src/configuring-zed.md +++ b/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.