Properly support prettier plugins

Kirill Bulatov created

Change summary

Cargo.lock                             |  1 
crates/language/src/language.rs        | 10 +--
crates/prettier/Cargo.toml             |  1 
crates/prettier/src/prettier.rs        | 72 +++++++++++++++++++++++----
crates/prettier/src/prettier_server.js |  1 
crates/project/src/project.rs          | 30 ++++++-----
crates/zed/src/languages/svelte.rs     |  9 +++
crates/zed/src/languages/tailwind.rs   |  9 +++
8 files changed, 100 insertions(+), 33 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5528,6 +5528,7 @@ dependencies = [
  "futures 0.3.28",
  "gpui",
  "language",
+ "log",
  "lsp",
  "node_runtime",
  "serde",

crates/language/src/language.rs 🔗

@@ -338,10 +338,6 @@ pub trait LspAdapter: 'static + Send + Sync {
         Default::default()
     }
 
-    // TODO kb enable this for
-    // markdown somehow?
-    // tailwind (needs a css plugin, there are 2 of them)
-    // svelte (needs a plugin)
     fn enabled_formatters(&self) -> Vec<BundledFormatter> {
         Vec::new()
     }
@@ -350,15 +346,15 @@ pub trait LspAdapter: 'static + Send + Sync {
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum BundledFormatter {
     Prettier {
-        parser_name: &'static str,
-        plugin_names: Vec<String>,
+        parser_name: Option<&'static str>,
+        plugin_names: Vec<&'static str>,
     },
 }
 
 impl BundledFormatter {
     pub fn prettier(parser_name: &'static str) -> Self {
         Self::Prettier {
-            parser_name,
+            parser_name: Some(parser_name),
             plugin_names: Vec::new(),
         }
     }

crates/prettier/Cargo.toml 🔗

@@ -15,6 +15,7 @@ lsp = { path = "../lsp" }
 node_runtime = { path = "../node_runtime"}
 util = { path = "../util" }
 
+log.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true

crates/prettier/src/prettier.rs 🔗

@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::Context;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use fs::Fs;
 use gpui::{AsyncAppContext, ModelHandle};
 use language::language_settings::language_settings;
@@ -29,6 +29,7 @@ pub struct LocateStart {
 pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
 pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 const PRETTIER_PACKAGE_NAME: &str = "prettier";
+const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
 
 impl Prettier {
     // This was taken from the prettier-vscode extension.
@@ -206,17 +207,64 @@ impl Prettier {
             let path = buffer_file
                 .map(|file| file.full_path(cx))
                 .map(|path| path.to_path_buf());
-            let parser = buffer_language.and_then(|language| {
-                language
-                    .lsp_adapters()
-                    .iter()
-                    .flat_map(|adapter| adapter.enabled_formatters())
-                    .find_map(|formatter| match formatter {
-                        BundledFormatter::Prettier { parser_name, .. } => {
-                            Some(parser_name.to_string())
+            let parsers_with_plugins = buffer_language
+                .into_iter()
+                .flat_map(|language| {
+                    language
+                        .lsp_adapters()
+                        .iter()
+                        .flat_map(|adapter| adapter.enabled_formatters())
+                        .filter_map(|formatter| match formatter {
+                            BundledFormatter::Prettier {
+                                parser_name,
+                                plugin_names,
+                            } => Some((parser_name, plugin_names)),
+                        })
+                })
+                .fold(
+                    HashMap::default(),
+                    |mut parsers_with_plugins, (parser_name, plugins)| {
+                        match parser_name {
+                            Some(parser_name) => parsers_with_plugins
+                                .entry(parser_name)
+                                .or_insert_with(HashSet::default)
+                                .extend(plugins),
+                            None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
+                                existing_plugins.extend(plugins.iter());
+                            }),
                         }
-                    })
-            });
+                        parsers_with_plugins
+                    },
+                );
+
+            let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
+            if parsers_with_plugins.len() > 1 {
+                log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
+            }
+
+            // TODO kb move the entire prettier server js file into *.mjs one instead?
+            let plugin_name_into_path = |plugin_name: &str| self.prettier_dir.join("node_modules").join(plugin_name).join("dist").join("index.mjs");
+            let (parser, plugins) = match selected_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 == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
+                            add_tailwind_back = true;
+                            false
+                        } else {
+                            true
+                        }
+                    }).map(|plugin_name| plugin_name_into_path(plugin_name)).collect::<Vec<_>>();
+                    if add_tailwind_back {
+                        plugins.push(plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME));
+                    }
+                    (Some(parser.to_string()), plugins)
+                },
+                None => (None, Vec::new()),
+            };
 
             let prettier_options = if self.default {
                 let language_settings = language_settings(buffer_language, buffer_file, cx);
@@ -246,6 +294,7 @@ impl Prettier {
                 text: buffer.text(),
                 options: FormatOptions {
                     parser,
+                    plugins,
                     // TODO kb is not absolute now
                     path,
                     prettier_options,
@@ -345,6 +394,7 @@ struct FormatParams {
 #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 struct FormatOptions {
+    plugins: Vec<PathBuf>,
     parser: Option<String>,
     #[serde(rename = "filepath")]
     path: Option<PathBuf>,

crates/prettier/src/prettier_server.js 🔗

@@ -153,6 +153,7 @@ async function handleMessage(message, prettier) {
         const options = {
             ...(params.options.prettierOptions || prettier.config),
             parser: params.options.parser,
+            plugins: params.options.plugins,
             path: params.options.path
         };
         // TODO kb always resolve prettier config for each file.

crates/project/src/project.rs 🔗

@@ -852,6 +852,7 @@ impl Project {
                         }
                     }
                     let worktree = buffer_file
+                        // TODO kb wrong usage (+ look around for another one like this)
                         .map(|f| f.worktree_id())
                         .map(WorktreeId::from_usize);
                     language_formatters_to_check.push((
@@ -912,7 +913,7 @@ impl Project {
         }
 
         for (worktree, language, settings) in language_formatters_to_check {
-            self.maybe_start_default_formatters(worktree, &language, &settings, cx);
+            self.install_default_formatters(worktree, &language, &settings, cx);
         }
 
         // Start all the newly-enabled language servers.
@@ -2669,7 +2670,7 @@ impl Project {
             .map(|f| f.worktree_id())
             .map(WorktreeId::from_usize);
         let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
-        self.maybe_start_default_formatters(worktree, &new_language, &settings, cx);
+        self.install_default_formatters(worktree, &new_language, &settings, cx);
 
         if let Some(file) = File::from_dyn(buffer_file.as_ref()) {
             let worktree = file.worktree.clone();
@@ -6395,7 +6396,7 @@ impl Project {
                 .prettier_instances
                 .iter()
                 .filter_map(|((worktree_id, prettier_path), prettier_task)| {
-                    if worktree_id == &Some(current_worktree_id) {
+                    if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) {
                         Some((*worktree_id, prettier_path.clone(), prettier_task.clone()))
                     } else {
                         None
@@ -6412,7 +6413,7 @@ impl Project {
                                 .await
                                 .with_context(|| {
                                     format!(
-                                        "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?}"
+                                        "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
                                     )
                                 })
                                 .map_err(Arc::new)
@@ -8410,6 +8411,7 @@ impl Project {
                                 .supplementary_language_servers
                                 .insert(new_server_id, (name, Arc::clone(prettier.server())));
                             // TODO kb could there be a race with multiple default prettier instances added?
+                            // also, clean up prettiers for dropped workspaces (e.g. external files that got closed)
                             cx.emit(Event::LanguageServerAdded(new_server_id));
                         });
                     }
@@ -8426,7 +8428,7 @@ impl Project {
         Some(task)
     }
 
-    fn maybe_start_default_formatters(
+    fn install_default_formatters(
         &self,
         worktree: Option<WorktreeId>,
         new_language: &Language,
@@ -8458,14 +8460,10 @@ impl Project {
         };
 
         let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
-        if let Some(_already_running) = self
+        let already_running_prettier = self
             .prettier_instances
             .get(&(worktree, default_prettier_dir.to_path_buf()))
-        {
-            // TODO kb need to compare plugins, install missing and restart prettier
-            // TODO kb move the entire prettier init logic into prettier.rs
-            return;
-        }
+            .cloned();
 
         let fs = Arc::clone(&self.fs);
         cx.background()
@@ -8478,8 +8476,7 @@ impl Project {
                 let packages_to_versions = future::try_join_all(
                     prettier_plugins
                         .iter()
-                        .map(|s| s.as_str())
-                        .chain(Some("prettier"))
+                        .chain(Some(&"prettier"))
                         .map(|package_name| async {
                             let returned_package_name = package_name.to_string();
                             let latest_version = node.npm_package_latest_version(package_name)
@@ -8497,6 +8494,13 @@ impl Project {
                     (package.as_str(), version.as_str())
                 }).collect::<Vec<_>>();
                 node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
+
+                if !prettier_plugins.is_empty() {
+                    if let Some(prettier) = already_running_prettier {
+                        prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?;
+                    }
+                }
+
                 anyhow::Ok(())
             })
             .detach_and_log_err(cx);

crates/zed/src/languages/svelte.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::json;
@@ -95,6 +95,13 @@ impl LspAdapter for SvelteLspAdapter {
             "provideFormatter": true
         }))
     }
+
+    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+        vec![BundledFormatter::Prettier {
+            parser_name: Some("svelte"),
+            plugin_names: vec!["prettier-plugin-svelte"],
+        }]
+    }
 }
 
 async fn get_cached_server_binary(

crates/zed/src/languages/tailwind.rs 🔗

@@ -6,7 +6,7 @@ use futures::{
     FutureExt, StreamExt,
 };
 use gpui::AppContext;
-use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::LanguageServerBinary;
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
@@ -127,6 +127,13 @@ impl LspAdapter for TailwindLspAdapter {
             .into_iter(),
         )
     }
+
+    fn enabled_formatters(&self) -> Vec<BundledFormatter> {
+        vec![BundledFormatter::Prettier {
+            parser_name: None,
+            plugin_names: vec!["prettier-plugin-tailwindcss"],
+        }]
+    }
 }
 
 async fn get_cached_server_binary(