Autodetect parser name with prettier by default (#11558)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/11517 

* Removes forced prettier parser name for languages, making `auto`
command to run prettier on every file by default.
* Moves prettier configs away from plugin language declarations into
language settings

Release Notes:

- N/A

Change summary

assets/settings/default.json                 | 159 ++++++++++++++++-----
crates/collab/src/tests/integration_tests.rs |  23 ++-
crates/editor/src/editor_tests.rs            |  36 +++-
crates/language/src/buffer.rs                |   1 
crates/language/src/language.rs              |  17 --
crates/language/src/language_registry.rs     |   9 -
crates/language/src/language_settings.rs     |  36 ++++
crates/languages/src/css/config.toml         |   1 
crates/languages/src/javascript/config.toml  |   1 
crates/languages/src/json/config.toml        |   1 
crates/languages/src/markdown/config.toml    |   1 
crates/languages/src/tsx/config.toml         |   1 
crates/languages/src/typescript/config.toml  |   1 
crates/languages/src/yaml/config.toml        |   1 
crates/prettier/src/prettier.rs              | 110 ++++++--------
crates/project/src/prettier_support.rs       |  49 ++----
crates/project/src/project.rs                |  57 +++++--
17 files changed, 293 insertions(+), 211 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -103,14 +103,7 @@
   // Hide the values of in variables from visual display in private files
   "redact_private_values": false,
   // Globs to match against file paths to determine if a file is private.
-  "private_files": [
-    "**/.env*",
-    "**/*.pem",
-    "**/*.key",
-    "**/*.cert",
-    "**/*.crt",
-    "**/secrets.yml"
-  ],
+  "private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
   // Whether to use additional LSP queries to format (and amend) the code after
   // every "trigger" symbol input, defined by LSP server capabilities.
   "use_on_type_format": true,
@@ -311,9 +304,7 @@
   // The list of language servers to use (or disable) for all languages.
   //
   // This is typically customized on a per-language basis.
-  "language_servers": [
-    "..."
-  ],
+  "language_servers": ["..."],
   // When to automatically save edited buffers. This setting can
   // take four values.
   //
@@ -448,9 +439,7 @@
   "copilot": {
     // The set of glob patterns for which copilot should be disabled
     // in any matching file.
-    "disabled_globs": [
-      ".env"
-    ]
+    "disabled_globs": [".env"]
   },
   // Settings specific to journaling
   "journal": {
@@ -561,12 +550,7 @@
         // Default directories to search for virtual environments, relative
         // to the current working directory. We recommend overriding this
         // in your project's settings, rather than globally.
-        "directories": [
-          ".env",
-          "env",
-          ".venv",
-          "venv"
-        ],
+        "directories": [".env", "env", ".venv", "venv"],
         // Can also be 'csh', 'fish', and `nushell`
         "activate_script": "default"
       }
@@ -609,19 +593,30 @@
   },
   // Different settings for specific languages.
   "languages": {
-    "C++": {
-      "format_on_save": "off"
+    "Astro": {
+      "prettier": {
+        "allowed": true,
+        "plugins": ["prettier-plugin-astro"]
+      }
+    },
+    "Blade": {
+      "prettier": {
+        "allowed": true
+      }
     },
     "C": {
       "format_on_save": "off"
     },
+    "C++": {
+      "format_on_save": "off"
+    },
+    "CSS": {
+      "prettier": {
+        "allowed": true
+      }
+    },
     "Elixir": {
-      "language_servers": [
-        "elixir-ls",
-        "!next-ls",
-        "!lexical",
-        "..."
-      ]
+      "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
     },
     "Gleam": {
       "tab_size": 2
@@ -631,32 +626,120 @@
         "source.organizeImports": true
       }
     },
+    "GraphQL": {
+      "prettier": {
+        "allowed": true
+      }
+    },
     "HEEX": {
-      "language_servers": [
-        "elixir-ls",
-        "!next-ls",
-        "!lexical",
-        "..."
-      ]
+      "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
+    },
+    "HTML": {
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "Java": {
+      "prettier": {
+        "allowed": true,
+        "plugins": ["prettier-plugin-java"]
+      }
+    },
+    "JavaScript": {
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "JSON": {
+      "prettier": {
+        "allowed": true
+      }
     },
     "Make": {
       "hard_tabs": true
     },
     "Markdown": {
-      "format_on_save": "off"
+      "format_on_save": "off",
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "PHP": {
+      "prettier": {
+        "allowed": true,
+        "plugins": ["@prettier/plugin-php"]
+      }
     },
     "Prisma": {
       "tab_size": 2
     },
     "Ruby": {
       "language_servers": ["solargraph", "!ruby-lsp", "..."]
+    },
+    "SCSS": {
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "SQL": {
+      "prettier": {
+        "allowed": true,
+        "plugins": ["prettier-plugin-sql"]
+      }
+    },
+    "Svelte": {
+      "prettier": {
+        "allowed": true,
+        "plugins": ["prettier-plugin-svelte"]
+      }
+    },
+    "TSX": {
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "Twig": {
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "TypeScript": {
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "Vue.js": {
+      "prettier": {
+        "allowed": true
+      }
+    },
+    "XML": {
+      "prettier": {
+        "allowed": true,
+        "plugins": ["@prettier/plugin-xml"]
+      }
+    },
+    "YAML": {
+      "prettier": {
+        "allowed": true
+      }
     }
   },
   // Zed's Prettier integration settings.
-  // If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
-  // project has no other Prettier installed.
+  // Allows to enable/disable formatting with Prettier
+  // and configure default Prettier, used when no project-level Prettier installation is found.
   "prettier": {
-    // Use regular Prettier json configuration:
+    // // Whether to consider prettier formatter or not when attempting to format a file.
+    // "allowed": false,
+    //
+    // // Use regular Prettier json configuration.
+    // // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
+    // // the project has no other Prettier installed.
+    // "plugins": [],
+    //
+    // // Use regular Prettier json configuration.
+    // // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
+    // // the project has no other Prettier installed.
     // "trailingComma": "es5",
     // "tabWidth": 4,
     // "semi": false,

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

@@ -17,7 +17,7 @@ use gpui::{
     MouseDownEvent, TestAppContext,
 };
 use language::{
-    language_settings::{AllLanguageSettings, Formatter},
+    language_settings::{AllLanguageSettings, Formatter, PrettierSettings},
     tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
     LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
 };
@@ -4445,18 +4445,17 @@ async fn test_prettier_formatting_buffer(
 
     client_a.language_registry().add(Arc::new(Language::new(
         LanguageConfig {
-            name: "Rust".into(),
+            name: "TypeScript".into(),
             matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
+                path_suffixes: vec!["ts".to_string()],
                 ..Default::default()
             },
-            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
     )));
     let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
-        "Rust",
+        "TypeScript",
         FakeLspAdapter {
             prettier_plugins: vec![test_plugin],
             ..Default::default()
@@ -4470,11 +4469,11 @@ async fn test_prettier_formatting_buffer(
     let buffer_text = "let one = \"two\"";
     client_a
         .fs()
-        .insert_tree(&directory, json!({ "a.rs": buffer_text }))
+        .insert_tree(&directory, json!({ "a.ts": buffer_text }))
         .await;
     let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
     let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
-    let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
+    let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
     let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
 
     let project_id = active_call_a
@@ -4482,13 +4481,17 @@ async fn test_prettier_formatting_buffer(
         .await
         .unwrap();
     let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
-    let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
+    let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
     let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
 
     cx_a.update(|cx| {
         cx.update_global(|store: &mut SettingsStore, cx| {
             store.update_user_settings::<AllLanguageSettings>(cx, |file| {
                 file.defaults.formatter = Some(Formatter::Auto);
+                file.defaults.prettier = Some(PrettierSettings {
+                    allowed: true,
+                    ..PrettierSettings::default()
+                });
             });
         });
     });
@@ -4496,6 +4499,10 @@ async fn test_prettier_formatting_buffer(
         cx.update_global(|store: &mut SettingsStore, cx| {
             store.update_user_settings::<AllLanguageSettings>(cx, |file| {
                 file.defaults.formatter = Some(Formatter::LanguageServer);
+                file.defaults.prettier = Some(PrettierSettings {
+                    allowed: true,
+                    ..PrettierSettings::default()
+                });
             });
         });
     });

crates/editor/src/editor_tests.rs 🔗

@@ -12,7 +12,9 @@ use futures::StreamExt;
 use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
 use indoc::indoc;
 use language::{
-    language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
+    language_settings::{
+        AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
+    },
     BracketPairConfig,
     Capability::ReadWrite,
     FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
@@ -6254,13 +6256,18 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
                 path_suffixes: vec!["rs".to_string()],
                 ..Default::default()
             },
-            // Enable Prettier formatting for the same buffer, and ensure
-            // LSP is called instead of Prettier.
-            prettier_parser_name: Some("test_parser".to_string()),
-            ..Default::default()
+            ..LanguageConfig::default()
         },
         Some(tree_sitter_rust::language()),
     )));
+    update_test_language_settings(cx, |settings| {
+        // Enable Prettier formatting for the same buffer, and ensure
+        // LSP is called instead of Prettier.
+        settings.defaults.prettier = Some(PrettierSettings {
+            allowed: true,
+            ..PrettierSettings::default()
+        });
+    });
     let mut fake_servers = language_registry.register_fake_lsp_adapter(
         "Rust",
         FakeLspAdapter {
@@ -8599,27 +8606,32 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     });
 
     let fs = FakeFs::new(cx.executor());
-    fs.insert_file("/file.rs", Default::default()).await;
+    fs.insert_file("/file.ts", Default::default()).await;
 
-    let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+    let project = Project::test(fs, ["/file.ts".as_ref()], cx).await;
     let language_registry = project.read_with(cx, |project, _| project.languages().clone());
 
     language_registry.add(Arc::new(Language::new(
         LanguageConfig {
-            name: "Rust".into(),
+            name: "TypeScript".into(),
             matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
+                path_suffixes: vec!["ts".to_string()],
                 ..Default::default()
             },
-            prettier_parser_name: Some("test_parser".to_string()),
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
     )));
+    update_test_language_settings(cx, |settings| {
+        settings.defaults.prettier = Some(PrettierSettings {
+            allowed: true,
+            ..PrettierSettings::default()
+        });
+    });
 
     let test_plugin = "test_plugin";
     let _ = language_registry.register_fake_lsp_adapter(
-        "Rust",
+        "TypeScript",
         FakeLspAdapter {
             prettier_plugins: vec![test_plugin],
             ..Default::default()
@@ -8628,7 +8640,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
 
     let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
     let buffer = project
-        .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+        .update(cx, |project, cx| project.open_local_buffer("/file.ts", cx))
         .await
         .unwrap();
 

crates/language/src/buffer.rs 🔗

@@ -479,6 +479,7 @@ pub struct Chunk<'a> {
 }
 
 /// A set of edits to a given version of a buffer, computed asynchronously.
+#[derive(Debug)]
 pub struct Diff {
     pub(crate) base_version: clock::Global,
     line_ending: LineEnding,

crates/language/src/language.rs 🔗

@@ -602,13 +602,6 @@ pub struct LanguageConfig {
     /// or a whole-word search in buffer search.
     #[serde(default)]
     pub word_characters: HashSet<char>,
-    /// The name of a Prettier parser that should be used for this language.
-    #[serde(default)]
-    pub prettier_parser_name: Option<String>,
-    /// The names of any Prettier plugins that should be used for this language.
-    #[serde(default)]
-    pub prettier_plugins: Vec<Arc<str>>,
-
     /// Whether to indent lines using tab characters, as opposed to multiple
     /// spaces.
     #[serde(default)]
@@ -700,8 +693,6 @@ impl Default for LanguageConfig {
             scope_opt_in_language_servers: Default::default(),
             overrides: Default::default(),
             word_characters: Default::default(),
-            prettier_parser_name: None,
-            prettier_plugins: Default::default(),
             collapsed_placeholder: Default::default(),
             hard_tabs: Default::default(),
             tab_size: Default::default(),
@@ -1375,14 +1366,6 @@ impl Language {
         }
     }
 
-    pub fn prettier_parser_name(&self) -> Option<&str> {
-        self.config.prettier_parser_name.as_deref()
-    }
-
-    pub fn prettier_plugins(&self) -> &Vec<Arc<str>> {
-        &self.config.prettier_plugins
-    }
-
     pub fn lsp_id(&self) -> String {
         match self.config.name.as_ref() {
             "Plain Text" => "plaintext".to_string(),

crates/language/src/language_registry.rs 🔗

@@ -715,15 +715,6 @@ impl LanguageRegistry {
             .unwrap_or_default()
     }
 
-    pub fn all_prettier_plugins(&self) -> Vec<Arc<str>> {
-        let state = self.state.read();
-        state
-            .languages
-            .iter()
-            .flat_map(|language| language.config.prettier_plugins.iter().cloned())
-            .collect()
-    }
-
     pub fn update_lsp_status(
         &self,
         server_name: LanguageServerName,

crates/language/src/language_settings.rs 🔗

@@ -89,9 +89,7 @@ pub struct LanguageSettings {
     /// How to perform a buffer format.
     pub formatter: Formatter,
     /// Zed's Prettier integration settings.
-    /// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
-    /// the project has no other Prettier installed.
-    pub prettier: HashMap<String, serde_json::Value>,
+    pub prettier: PrettierSettings,
     /// Whether to use language servers to provide code intelligence.
     pub enable_language_server: bool,
     /// The list of language servers to use (or disable) for this language.
@@ -267,12 +265,12 @@ pub struct LanguageSettingsContent {
     #[serde(default)]
     pub formatter: Option<Formatter>,
     /// Zed's Prettier integration settings.
-    /// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
-    /// the project has no other Prettier installed.
+    /// Allows to enable/disable formatting with Prettier
+    /// and configure default Prettier, used when no project-level Prettier installation is found.
     ///
-    /// Default: {}
+    /// Default: off
     #[serde(default)]
-    pub prettier: Option<HashMap<String, serde_json::Value>>,
+    pub prettier: Option<PrettierSettings>,
     /// Whether to use language servers to provide code intelligence.
     ///
     /// Default: true
@@ -751,6 +749,30 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
     merge(&mut settings.inlay_hints, src.inlay_hints);
 }
 
+/// Allows to enable/disable formatting with Prettier
+/// and configure default Prettier, used when no project-level Prettier installation is found.
+/// Prettier formatting is disabled by default.
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct PrettierSettings {
+    /// Enables or disables formatting with Prettier for a given language.
+    #[serde(default)]
+    pub allowed: bool,
+
+    /// Forces Prettier integration to use a specific parser name when formatting files with the language.
+    #[serde(default)]
+    pub parser: Option<String>,
+
+    /// Forces Prettier integration to use specific plugins when formatting files with the language.
+    /// The default Prettier will be installed with these plugins.
+    #[serde(default)]
+    pub plugins: HashSet<String>,
+
+    /// Default Prettier options, in the format as in package.json section for Prettier.
+    /// If project installs Prettier via its package.json, these options will be ignored.
+    #[serde(flatten)]
+    pub options: HashMap<String, serde_json::Value>,
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/languages/src/javascript/config.toml 🔗

@@ -17,7 +17,6 @@ brackets = [
 word_characters = ["$", "#"]
 tab_size = 2
 scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
-prettier_parser_name = "babel"
 
 [overrides.element]
 line_comments = { remove = true }

crates/languages/src/json/config.toml 🔗

@@ -8,5 +8,4 @@ brackets = [
     { start = "[", end = "]", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
 ]
-prettier_parser_name = "json"
 tab_size = 2

crates/languages/src/markdown/config.toml 🔗

@@ -11,7 +11,6 @@ brackets = [
     { start = "'", end = "'", close = false, newline = false },
     { start = "`", end = "`", close = false, newline = false },
 ]
-prettier_parser_name = "markdown"
 
 tab_size = 2
 soft_wrap = "preferred_line_length"

crates/languages/src/tsx/config.toml 🔗

@@ -15,7 +15,6 @@ brackets = [
 ]
 word_characters = ["#", "$"]
 scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
-prettier_parser_name = "typescript"
 tab_size = 2
 
 [overrides.element]

crates/languages/src/typescript/config.toml 🔗

@@ -14,5 +14,4 @@ brackets = [
     { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
 ]
 word_characters = ["#", "$"]
-prettier_parser_name = "typescript"
 tab_size = 2

crates/prettier/src/prettier.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::Context;
 use collections::{HashMap, HashSet};
 use fs::Fs;
 use gpui::{AsyncAppContext, Model};
-use language::{language_settings::language_settings, Buffer, Diff, LanguageRegistry};
+use language::{language_settings::language_settings, Buffer, Diff};
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
@@ -25,7 +25,6 @@ pub struct RealPrettier {
     default: bool,
     prettier_dir: PathBuf,
     server: Arc<LanguageServer>,
-    language_registry: Arc<LanguageRegistry>,
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -156,7 +155,6 @@ impl Prettier {
         _: LanguageServerId,
         prettier_dir: PathBuf,
         _: Arc<dyn NodeRuntime>,
-        _: Arc<LanguageRegistry>,
         _: AsyncAppContext,
     ) -> anyhow::Result<Self> {
         Ok(Self::Test(TestPrettier {
@@ -170,7 +168,6 @@ impl Prettier {
         server_id: LanguageServerId,
         prettier_dir: PathBuf,
         node: Arc<dyn NodeRuntime>,
-        language_registry: Arc<LanguageRegistry>,
         cx: AsyncAppContext,
     ) -> anyhow::Result<Self> {
         use lsp::LanguageServerBinary;
@@ -209,7 +206,6 @@ impl Prettier {
         Ok(Self::Real(RealPrettier {
             server,
             default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
-            language_registry,
             prettier_dir,
         }))
     }
@@ -225,22 +221,19 @@ impl Prettier {
                 let params = buffer
                     .update(cx, |buffer, cx| {
                         let buffer_language = buffer.language();
-                        let parser_with_plugins = buffer_language.and_then(|l| {
-                            let prettier_parser = l.prettier_parser_name()?;
-                            let mut prettier_plugins =
-                                local.language_registry.all_prettier_plugins();
-                            prettier_plugins.dedup();
-                            Some((prettier_parser, prettier_plugins))
-                        });
-
+                        let language_settings = language_settings(buffer_language, buffer.file(), cx);
+                        let prettier_settings = &language_settings.prettier;
+                        anyhow::ensure!(
+                            prettier_settings.allowed,
+                            "Cannot format: prettier is not allowed for language {buffer_language:?}"
+                        );
                         let prettier_node_modules = self.prettier_dir().join("node_modules");
                         anyhow::ensure!(
                             prettier_node_modules.is_dir(),
                             "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
                         );
-                        let plugin_name_into_path = |plugin_name: Arc<str>| {
-                            let prettier_plugin_dir =
-                                prettier_node_modules.join(plugin_name.as_ref());
+                        let plugin_name_into_path = |plugin_name: &str| {
+                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
                             [
                                 prettier_plugin_dir.join("dist").join("index.mjs"),
                                 prettier_plugin_dir.join("dist").join("index.js"),
@@ -255,45 +248,34 @@ impl Prettier {
                             .into_iter()
                             .find(|possible_plugin_path| possible_plugin_path.is_file())
                         };
-                        let (parser, located_plugins) = match parser_with_plugins {
-                            Some((parser, plugins)) => {
-                                // Tailwind plugin requires being added last
-                                // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
-                                let mut add_tailwind_back = false;
-
-                                let mut plugins = plugins
-                                    .into_iter()
-                                    .filter(|plugin_name| {
-                                        if plugin_name.as_ref()
-                                            == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME
-                                        {
-                                            add_tailwind_back = true;
-                                            false
-                                        } else {
-                                            true
-                                        }
-                                    })
-                                    .map(|plugin_name| {
-                                        (plugin_name.clone(), plugin_name_into_path(plugin_name))
-                                    })
-                                    .collect::<Vec<_>>();
-                                if add_tailwind_back {
-                                    plugins.push((
-                                        TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.into(),
-                                        plugin_name_into_path(
-                                            TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.into(),
-                                        ),
-                                    ));
+
+                        // Tailwind plugin requires being added last
+                        // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
+                        let mut add_tailwind_back = false;
+
+                        let mut located_plugins = prettier_settings.plugins.iter()
+                            .filter(|plugin_name| {
+                                if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
+                                    add_tailwind_back = true;
+                                    false
+                                } else {
+                                    true
                                 }
-                                (Some(parser.to_string()), plugins)
-                            }
-                            None => (None, Vec::new()),
-                        };
+                            })
+                            .map(|plugin_name| {
+                                let plugin_path = plugin_name_into_path(plugin_name);
+                                (plugin_name.clone(), plugin_path)
+                            })
+                            .collect::<Vec<_>>();
+                        if add_tailwind_back {
+                            located_plugins.push((
+                                TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(),
+                                plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME),
+                            ));
+                        }
 
                         let prettier_options = if self.is_default() {
-                            let language_settings =
-                                language_settings(buffer_language, buffer.file(), cx);
-                            let mut options = language_settings.prettier.clone();
+                            let mut options = prettier_settings.options.clone();
                             if !options.contains_key("tabWidth") {
                                 options.insert(
                                     "tabWidth".to_string(),
@@ -321,11 +303,7 @@ impl Prettier {
                                 match located_plugin_path {
                                     Some(path) => Some(path),
                                     None => {
-                                        log::error!(
-                                            "Have not found plugin path for {:?} inside {:?}",
-                                            plugin_name,
-                                            prettier_node_modules
-                                        );
+                                        log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
                                         None
                                     }
                                 }
@@ -341,7 +319,7 @@ impl Prettier {
                         anyhow::Ok(FormatParams {
                             text: buffer.text(),
                             options: FormatOptions {
-                                parser,
+                                parser: prettier_settings.parser.clone(),
                                 plugins,
                                 path: buffer_path,
                                 prettier_options,
@@ -360,9 +338,19 @@ impl Prettier {
             #[cfg(any(test, feature = "test-support"))]
             Self::Test(_) => Ok(buffer
                 .update(cx, |buffer, cx| {
-                    let formatted_text = buffer.text() + FORMAT_SUFFIX;
-                    buffer.diff(formatted_text, cx)
-                })?
+                    match buffer
+                        .language()
+                        .map(|language| language.lsp_id())
+                        .as_deref()
+                    {
+                        Some("rust") => anyhow::bail!("prettier does not support Rust"),
+                        Some(_other) => {
+                            let formatted_text = buffer.text() + FORMAT_SUFFIX;
+                            Ok(buffer.diff(formatted_text, cx))
+                        }
+                        None => panic!("Should not format buffer without a language with prettier"),
+                    }
+                })??
                 .await),
         }
     }

crates/project/src/prettier_support.rs 🔗

@@ -14,7 +14,7 @@ use futures::{
 use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
 use language::{
     language_settings::{Formatter, LanguageSettings},
-    Buffer, Language, LanguageServerName, LocalFile,
+    Buffer, LanguageServerName, LocalFile,
 };
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
@@ -25,20 +25,12 @@ use crate::{
     Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
 };
 
-pub fn prettier_plugins_for_language<'a>(
-    language: &'a Arc<Language>,
+pub fn prettier_plugins_for_language(
     language_settings: &LanguageSettings,
-) -> Option<&'a Vec<Arc<str>>> {
+) -> Option<&HashSet<String>> {
     match &language_settings.formatter {
-        Formatter::Prettier { .. } | Formatter::Auto => {}
-        Formatter::LanguageServer | Formatter::External { .. } | Formatter::CodeActions(_) => {
-            return None
-        }
-    };
-    if language.prettier_parser_name().is_some() {
-        Some(language.prettier_plugins())
-    } else {
-        None
+        Formatter::Prettier { .. } | Formatter::Auto => Some(&language_settings.prettier.plugins),
+        Formatter::LanguageServer | Formatter::External { .. } | Formatter::CodeActions(_) => None,
     }
 }
 
@@ -110,6 +102,7 @@ pub struct DefaultPrettier {
     installed_plugins: HashSet<Arc<str>>,
 }
 
+#[derive(Debug)]
 pub enum PrettierInstallation {
     NotInstalled {
         attempts: usize,
@@ -121,7 +114,7 @@ pub enum PrettierInstallation {
 
 pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct PrettierInstance {
     attempt: usize,
     prettier: Option<PrettierTask>,
@@ -295,20 +288,15 @@ fn start_prettier(
 ) -> PrettierTask {
     cx.spawn(|project, mut cx| async move {
         log::info!("Starting prettier at path {prettier_dir:?}");
-        let language_registry = project.update(&mut cx, |project, _| project.languages.clone())?;
-        let new_server_id = language_registry.next_language_server_id();
-
-        let new_prettier = Prettier::start(
-            new_server_id,
-            prettier_dir,
-            node,
-            language_registry,
-            cx.clone(),
-        )
-        .await
-        .context("default prettier spawn")
-        .map(Arc::new)
-        .map_err(Arc::new)?;
+        let new_server_id = project.update(&mut cx, |project, _| {
+            project.languages.next_language_server_id()
+        })?;
+
+        let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
+            .await
+            .context("default prettier spawn")
+            .map(Arc::new)
+            .map_err(Arc::new)?;
         register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
         Ok(new_prettier)
     })
@@ -526,10 +514,7 @@ impl Project {
         }
         let buffer = buffer.read(cx);
         let buffer_file = buffer.file();
-        let Some(buffer_language) = buffer.language() else {
-            return Task::ready(None);
-        };
-        if buffer_language.prettier_parser_name().is_none() {
+        if buffer.language().is_none() {
             return Task::ready(None);
         }
         let Some(node) = self.node.clone() else {

crates/project/src/project.rs 🔗

@@ -550,6 +550,7 @@ pub enum FormatTrigger {
 
 // Currently, formatting operations are represented differently depending on
 // whether they come from a language server or an external command.
+#[derive(Debug)]
 enum FormatOperation {
     Lsp(Vec<(Range<Anchor>, String)>),
     External(Diff),
@@ -1088,11 +1089,8 @@ impl Project {
                                 .push((file.worktree.clone(), Arc::clone(language)));
                         }
                     }
-                    language_formatters_to_check.push((
-                        buffer_file.map(|f| f.worktree_id(cx)),
-                        Arc::clone(language),
-                        settings.clone(),
-                    ));
+                    language_formatters_to_check
+                        .push((buffer_file.map(|f| f.worktree_id(cx)), settings.clone()));
                 }
             }
         }
@@ -1148,9 +1146,9 @@ impl Project {
         }
 
         let mut prettier_plugins_by_worktree = HashMap::default();
-        for (worktree, language, settings) in language_formatters_to_check {
+        for (worktree, language_settings) in language_formatters_to_check {
             if let Some(plugins) =
-                prettier_support::prettier_plugins_for_language(&language, &settings)
+                prettier_support::prettier_plugins_for_language(&language_settings)
             {
                 prettier_plugins_by_worktree
                     .entry(worktree)
@@ -1159,7 +1157,11 @@ impl Project {
             }
         }
         for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
-            self.install_default_prettier(worktree, prettier_plugins.into_iter(), cx);
+            self.install_default_prettier(
+                worktree,
+                prettier_plugins.into_iter().map(Arc::from),
+                cx,
+            );
         }
 
         // Start all the newly-enabled language servers.
@@ -3070,10 +3072,12 @@ impl Project {
         let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
         let buffer_file = File::from_dyn(buffer_file.as_ref());
         let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
-        if let Some(prettier_plugins) =
-            prettier_support::prettier_plugins_for_language(&new_language, &settings)
-        {
-            self.install_default_prettier(worktree, prettier_plugins.iter().cloned(), cx);
+        if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language(&settings) {
+            self.install_default_prettier(
+                worktree,
+                prettier_plugins.iter().map(|s| Arc::from(s.as_str())),
+                cx,
+            );
         };
         if let Some(file) = buffer_file {
             let worktree = file.worktree.clone();
@@ -4786,6 +4790,11 @@ impl Project {
                 .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 => {}
 
@@ -4845,11 +4854,18 @@ impl Project {
                     }
                 }
                 (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
-                    let prettier =
-                        prettier_support::format_with_prettier(&project, buffer, &mut cx).await;
+                    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?);
+                        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(
@@ -4866,11 +4882,12 @@ impl Project {
                     }
                 }
                 (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
-                    let prettier =
-                        prettier_support::format_with_prettier(&project, buffer, &mut cx).await;
-
-                    if let Some(operation) = prettier {
-                        format_operation = Some(operation?);
+                    if prettier_settings.allowed {
+                        if let Some(operation) =
+                            prettier_support::format_with_prettier(&project, buffer, &mut cx).await
+                        {
+                            format_operation = Some(operation?);
+                        }
                     }
                 }
             };