Install default prettier and plugins on startup

Kirill Bulatov created

Change summary

crates/prettier/src/prettier.rs |   7 -
crates/project/src/project.rs   | 124 +++++++++++++++++++++++++++++++---
2 files changed, 113 insertions(+), 18 deletions(-)

Detailed changes

crates/prettier/src/prettier.rs 🔗

@@ -204,10 +204,3 @@ async fn find_closest_prettier_dir(
     }
     Ok(None)
 }
-
-async fn prepare_default_prettier(
-    fs: Arc<dyn Fs>,
-    node: Arc<dyn NodeRuntime>,
-) -> anyhow::Result<PathBuf> {
-    todo!("TODO kb need to call per language that supports it, and have to use extra packages sometimes")
-}

crates/project/src/project.rs 🔗

@@ -20,7 +20,7 @@ use futures::{
         mpsc::{self, UnboundedReceiver},
         oneshot,
     },
-    future::{try_join_all, Shared},
+    future::{self, try_join_all, Shared},
     stream::FuturesUnordered,
     AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
 };
@@ -31,7 +31,9 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::{
-    language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
+    language_settings::{
+        language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
+    },
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -79,8 +81,11 @@ use std::{
 use terminals::Terminals;
 use text::Anchor;
 use util::{
-    debug_panic, defer, http::HttpClient, merge_json_value_into,
-    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
+    debug_panic, defer,
+    http::HttpClient,
+    merge_json_value_into,
+    paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
+    post_inc, ResultExt, TryFutureExt as _,
 };
 
 pub use fs::*;
@@ -832,17 +837,28 @@ impl Project {
 
     fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
         let mut language_servers_to_start = Vec::new();
+        let mut language_formatters_to_check = Vec::new();
         for buffer in self.opened_buffers.values() {
             if let Some(buffer) = buffer.upgrade(cx) {
                 let buffer = buffer.read(cx);
-                if let Some((file, language)) = buffer.file().zip(buffer.language()) {
-                    let settings = language_settings(Some(language), Some(file), cx);
+                let buffer_file = buffer.file();
+                let buffer_language = buffer.language();
+                let settings = language_settings(buffer_language, buffer_file, cx);
+                if let Some(language) = buffer_language {
                     if settings.enable_language_server {
-                        if let Some(file) = File::from_dyn(Some(file)) {
+                        if let Some(file) = File::from_dyn(buffer_file) {
                             language_servers_to_start
-                                .push((file.worktree.clone(), language.clone()));
+                                .push((file.worktree.clone(), Arc::clone(language)));
                         }
                     }
+                    let worktree = buffer_file
+                        .map(|f| f.worktree_id())
+                        .map(WorktreeId::from_usize);
+                    language_formatters_to_check.push((
+                        worktree,
+                        Arc::clone(language),
+                        settings.clone(),
+                    ));
                 }
             }
         }
@@ -895,6 +911,11 @@ impl Project {
                 .detach();
         }
 
+        // TODO kb restart all formatters if settings change
+        for (worktree, language, settings) in language_formatters_to_check {
+            self.maybe_start_default_formatters(worktree, &language, &settings, cx);
+        }
+
         // Start all the newly-enabled language servers.
         for (worktree, language) in language_servers_to_start {
             let worktree_path = worktree.read(cx).abs_path();
@@ -2643,7 +2664,15 @@ impl Project {
             }
         });
 
-        if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
+        let buffer_file = buffer.read(cx).file().cloned();
+        let worktree = buffer_file
+            .as_ref()
+            .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);
+
+        if let Some(file) = File::from_dyn(buffer_file.as_ref()) {
             let worktree = file.worktree.clone();
             if let Some(tree) = worktree.read(cx).as_local() {
                 self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
@@ -2651,8 +2680,6 @@ impl Project {
         }
     }
 
-    // TODO kb 2 usages of this method (buffer language select + settings change) should take care of
-    // `LspAdapter::enabled_formatters` collecting and initializing. Remove `Option<WorktreeId>` for prettier instances?
     fn start_language_servers(
         &mut self,
         worktree: &ModelHandle<Worktree>,
@@ -8279,6 +8306,81 @@ impl Project {
         });
         Some(task)
     }
+
+    fn maybe_start_default_formatters(
+        &mut self,
+        worktree: Option<WorktreeId>,
+        new_language: &Language,
+        language_settings: &LanguageSettings,
+        cx: &mut ModelContext<Self>,
+    ) {
+        match &language_settings.formatter {
+            Formatter::Prettier { .. } | Formatter::Auto => {}
+            Formatter::LanguageServer | Formatter::External { .. } => return,
+        };
+        let Some(node) = self.node.as_ref().cloned() else {
+            return;
+        };
+
+        let mut prettier_plugins = None;
+        for formatter in new_language
+            .lsp_adapters()
+            .into_iter()
+            .flat_map(|adapter| adapter.enabled_formatters())
+        {
+            match formatter {
+                BundledFormatter::Prettier { plugin_names } => prettier_plugins
+                    .get_or_insert_with(|| HashSet::default())
+                    .extend(plugin_names),
+            }
+        }
+        let Some(prettier_plugins) = prettier_plugins else {
+            return;
+        };
+
+        let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
+        if let Some(_already_running) = self
+            .prettier_instances
+            .get(&(worktree, default_prettier_dir.to_path_buf()))
+        {
+            // TODO kb need to compare plugins, install missing and restart prettier
+            return;
+        }
+
+        let fs = Arc::clone(&self.fs);
+        cx.background()
+            .spawn(async move {
+                let prettier_dir_metadata = fs.metadata(default_prettier_dir).await.with_context(|| format!("fetching FS metadata for prettier default dir {default_prettier_dir:?}"))?;
+                if prettier_dir_metadata.is_none() {
+                    fs.create_dir(default_prettier_dir).await.with_context(|| format!("creating prettier default dir {default_prettier_dir:?}"))?;
+                }
+
+                let packages_to_versions = future::try_join_all(
+                    prettier_plugins
+                        .iter()
+                        .map(|s| s.as_str())
+                        .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)
+                                .await
+                                .with_context(|| {
+                                    format!("fetching latest npm version for package {returned_package_name}")
+                                })?;
+                            anyhow::Ok((returned_package_name, latest_version))
+                        }),
+                )
+                .await
+                .context("fetching latest npm versions")?;
+
+                let borrowed_packages = packages_to_versions.iter().map(|(package, version)| {
+                    (package.as_str(), version.as_str())
+                }).collect::<Vec<_>>();
+                node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+    }
 }
 
 fn subscribe_for_copilot_events(