Refactor prettier (#17977)

Conrad Irwin and Mikayla created

In preparation for making formatting work on ssh remotes

Release Notes:

- N/A

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/node_runtime/src/node_runtime.rs      |   49 
crates/project/src/lsp_store.rs              |  194 +++
crates/project/src/prettier_store.rs         | 1122 ++++++++++++---------
crates/project/src/project.rs                |  218 ---
crates/remote_server/Cargo.toml              |    1 
crates/remote_server/src/headless_project.rs |   14 
6 files changed, 902 insertions(+), 696 deletions(-)

Detailed changes

crates/node_runtime/src/node_runtime.rs 🔗

@@ -462,3 +462,52 @@ impl NodeRuntime for FakeNodeRuntime {
         unreachable!("Should not install packages {packages:?}")
     }
 }
+
+// TODO: Remove this when headless binary can  run node
+pub struct DummyNodeRuntime;
+
+impl DummyNodeRuntime {
+    pub fn new() -> Arc<dyn NodeRuntime> {
+        Arc::new(Self)
+    }
+}
+
+#[async_trait::async_trait]
+impl NodeRuntime for DummyNodeRuntime {
+    async fn binary_path(&self) -> anyhow::Result<PathBuf> {
+        anyhow::bail!("Dummy Node Runtime")
+    }
+
+    async fn node_environment_path(&self) -> anyhow::Result<OsString> {
+        anyhow::bail!("Dummy node runtime")
+    }
+
+    async fn run_npm_subcommand(
+        &self,
+        _: Option<&Path>,
+        _subcommand: &str,
+        _args: &[&str],
+    ) -> anyhow::Result<Output> {
+        anyhow::bail!("Dummy node runtime")
+    }
+
+    async fn npm_package_latest_version(&self, _name: &str) -> anyhow::Result<String> {
+        anyhow::bail!("Dummy node runtime")
+    }
+
+    async fn npm_package_installed_version(
+        &self,
+        _local_package_directory: &Path,
+        _name: &str,
+    ) -> Result<Option<String>> {
+        anyhow::bail!("Dummy node runtime")
+    }
+
+    async fn npm_install_packages(
+        &self,
+        _: &Path,
+        _packages: &[(&str, &str)],
+    ) -> anyhow::Result<()> {
+        anyhow::bail!("Dummy node runtime")
+    }
+}

crates/project/src/lsp_store.rs 🔗

@@ -3,6 +3,7 @@ use crate::{
     environment::ProjectEnvironment,
     lsp_command::{self, *},
     lsp_ext_command,
+    prettier_store::{self, PrettierStore, PrettierStoreEvent},
     project_settings::{LspSettings, ProjectSettings},
     relativize_path, resolve_path,
     worktree_store::{WorktreeStore, WorktreeStoreEvent},
@@ -101,6 +102,8 @@ pub struct LocalLspStore {
         HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
     supplementary_language_servers:
         HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
+    prettier_store: Model<PrettierStore>,
+    current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     _subscription: gpui::Subscription,
 }
 
@@ -135,6 +138,7 @@ impl RemoteLspStore {}
 
 pub struct SshLspStore {
     upstream_client: AnyProtoClient,
+    current_lsp_settings: HashMap<Arc<str>, LspSettings>,
 }
 
 #[allow(clippy::large_enum_variant)]
@@ -310,9 +314,32 @@ impl LspStore {
         }
     }
 
+    pub fn swap_current_lsp_settings(
+        &mut self,
+        new_settings: HashMap<Arc<str>, LspSettings>,
+    ) -> Option<HashMap<Arc<str>, LspSettings>> {
+        match &mut self.mode {
+            LspStoreMode::Ssh(SshLspStore {
+                current_lsp_settings,
+                ..
+            })
+            | LspStoreMode::Local(LocalLspStore {
+                current_lsp_settings,
+                ..
+            }) => {
+                let ret = mem::take(current_lsp_settings);
+                *current_lsp_settings = new_settings;
+                Some(ret)
+            }
+            LspStoreMode::Remote(_) => None,
+        }
+    }
+
+    #[allow(clippy::too_many_arguments)]
     pub fn new_local(
         buffer_store: Model<BufferStore>,
         worktree_store: Model<WorktreeStore>,
+        prettier_store: Model<PrettierStore>,
         environment: Model<ProjectEnvironment>,
         languages: Arc<LanguageRegistry>,
         http_client: Option<Arc<dyn HttpClient>>,
@@ -324,6 +351,10 @@ impl LspStore {
             .detach();
         cx.subscribe(&worktree_store, Self::on_worktree_store_event)
             .detach();
+        cx.subscribe(&prettier_store, Self::on_prettier_store_event)
+            .detach();
+        cx.observe_global::<SettingsStore>(Self::on_settings_changed)
+            .detach();
 
         Self {
             mode: LspStoreMode::Local(LocalLspStore {
@@ -332,6 +363,8 @@ impl LspStore {
                 last_workspace_edits_by_language_server: Default::default(),
                 language_server_watched_paths: Default::default(),
                 language_server_watcher_registrations: Default::default(),
+                current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
+                prettier_store,
                 environment,
                 http_client,
                 fs,
@@ -387,9 +420,14 @@ impl LspStore {
             .detach();
         cx.subscribe(&worktree_store, Self::on_worktree_store_event)
             .detach();
+        cx.observe_global::<SettingsStore>(Self::on_settings_changed)
+            .detach();
 
         Self {
-            mode: LspStoreMode::Ssh(SshLspStore { upstream_client }),
+            mode: LspStoreMode::Ssh(SshLspStore {
+                upstream_client,
+                current_lsp_settings: Default::default(),
+            }),
             downstream_client: None,
             project_id,
             buffer_store,
@@ -401,6 +439,7 @@ impl LspStore {
             buffer_snapshots: Default::default(),
             next_diagnostic_group_id: Default::default(),
             diagnostic_summaries: Default::default(),
+
             diagnostics: Default::default(),
             active_entry: None,
             _maintain_workspace_config: Self::maintain_workspace_config(cx),
@@ -498,6 +537,36 @@ impl LspStore {
         }
     }
 
+    fn on_prettier_store_event(
+        &mut self,
+        _: Model<PrettierStore>,
+        event: &PrettierStoreEvent,
+        cx: &mut ModelContext<Self>,
+    ) {
+        match event {
+            PrettierStoreEvent::LanguageServerRemoved(prettier_server_id) => {
+                self.unregister_supplementary_language_server(*prettier_server_id, cx);
+            }
+            PrettierStoreEvent::LanguageServerAdded {
+                new_server_id,
+                name,
+                prettier_server,
+            } => {
+                self.register_supplementary_language_server(
+                    *new_server_id,
+                    name.clone(),
+                    prettier_server.clone(),
+                    cx,
+                );
+            }
+        }
+    }
+
+    // todo!
+    pub fn prettier_store(&self) -> Option<Model<PrettierStore>> {
+        self.as_local().map(|local| local.prettier_store.clone())
+    }
+
     fn on_buffer_event(
         &mut self,
         buffer: Model<Buffer>,
@@ -656,11 +725,29 @@ impl LspStore {
         });
 
         let buffer_file = buffer.read(cx).file().cloned();
+        let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
         let buffer_file = File::from_dyn(buffer_file.as_ref());
 
-        if let Some(file) = buffer_file {
+        let worktree_id = if let Some(file) = buffer_file {
             let worktree = file.worktree.clone();
-            self.start_language_servers(&worktree, new_language.name(), cx)
+            self.start_language_servers(&worktree, new_language.name(), cx);
+
+            Some(worktree.read(cx).id())
+        } else {
+            None
+        };
+
+        if let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) {
+            let prettier_store = self.as_local().map(|s| s.prettier_store.clone());
+            if let Some(prettier_store) = prettier_store {
+                prettier_store.update(cx, |prettier_store, cx| {
+                    prettier_store.install_default_prettier(
+                        worktree_id,
+                        prettier_plugins.iter().map(|s| Arc::from(s.as_str())),
+                        cx,
+                    )
+                })
+            }
         }
 
         cx.emit(LspStoreEvent::LanguageDetected {
@@ -799,6 +886,95 @@ impl LspStore {
         Task::ready(Ok(Default::default()))
     }
 
+    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.buffer_store.read(cx).buffers() {
+            let buffer = buffer.read(cx);
+            let buffer_file = File::from_dyn(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) = buffer_file {
+                        language_servers_to_start.push((file.worktree.clone(), language.name()));
+                    }
+                }
+                language_formatters_to_check
+                    .push((buffer_file.map(|f| f.worktree_id(cx)), settings.clone()));
+            }
+        }
+
+        let mut language_servers_to_stop = Vec::new();
+        let mut language_servers_to_restart = Vec::new();
+        let languages = self.languages.to_vec();
+
+        let new_lsp_settings = ProjectSettings::get_global(cx).lsp.clone();
+        let Some(current_lsp_settings) = self.swap_current_lsp_settings(new_lsp_settings.clone())
+        else {
+            return;
+        };
+        for (worktree_id, started_lsp_name) in self.started_language_servers() {
+            let language = languages.iter().find_map(|l| {
+                let adapter = self
+                    .languages
+                    .lsp_adapters(&l.name())
+                    .iter()
+                    .find(|adapter| adapter.name == started_lsp_name)?
+                    .clone();
+                Some((l, adapter))
+            });
+            if let Some((language, adapter)) = language {
+                let worktree = self.worktree_for_id(worktree_id, cx).ok();
+                let file = worktree.as_ref().and_then(|tree| {
+                    tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
+                });
+                if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
+                    language_servers_to_stop.push((worktree_id, started_lsp_name.clone()));
+                } else if let Some(worktree) = worktree {
+                    let server_name = &adapter.name.0;
+                    match (
+                        current_lsp_settings.get(server_name),
+                        new_lsp_settings.get(server_name),
+                    ) {
+                        (None, None) => {}
+                        (Some(_), None) | (None, Some(_)) => {
+                            language_servers_to_restart.push((worktree, language.name()));
+                        }
+                        (Some(current_lsp_settings), Some(new_lsp_settings)) => {
+                            if current_lsp_settings != new_lsp_settings {
+                                language_servers_to_restart.push((worktree, language.name()));
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        for (worktree_id, adapter_name) in language_servers_to_stop {
+            self.stop_language_server(worktree_id, adapter_name, cx)
+                .detach();
+        }
+
+        if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) {
+            prettier_store.update(cx, |prettier_store, cx| {
+                prettier_store.on_settings_changed(language_formatters_to_check, cx)
+            })
+        }
+
+        // Start all the newly-enabled language servers.
+        for (worktree, language) in language_servers_to_start {
+            self.start_language_servers(&worktree, language, cx);
+        }
+
+        // Restart all language servers with changed initialization options.
+        for (worktree, language) in language_servers_to_restart {
+            self.restart_language_servers(worktree, language, cx);
+        }
+
+        cx.notify();
+    }
+
     pub async fn execute_code_actions_on_servers(
         this: &WeakModel<LspStore>,
         adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
@@ -2375,7 +2551,7 @@ impl LspStore {
             })
     }
 
-    pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
+    fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
         self.diagnostics.remove(&id_to_remove);
         self.diagnostic_summaries.remove(&id_to_remove);
 
@@ -2406,6 +2582,12 @@ impl LspStore {
             }
             cx.emit(LspStoreEvent::LanguageServerRemoved(server_id_to_remove));
         }
+
+        if let Some(local) = self.as_local() {
+            local.prettier_store.update(cx, |prettier_store, cx| {
+                prettier_store.remove_worktree(id_to_remove, cx);
+            })
+        }
     }
 
     pub fn shared(
@@ -6117,6 +6299,10 @@ impl LspStore {
 
         let Some(local) = self.as_local() else { return };
 
+        local.prettier_store.update(cx, |prettier_store, cx| {
+            prettier_store.update_prettier_settings(&worktree_handle, changes, cx)
+        });
+
         let worktree_id = worktree_handle.read(cx).id();
         let mut language_server_ids = self
             .language_server_ids

crates/project/src/prettier_support.rs → crates/project/src/prettier_store.rs 🔗

@@ -5,444 +5,384 @@ use std::{
 };
 
 use anyhow::{anyhow, Context, Result};
-use collections::HashSet;
+use collections::{HashMap, HashSet};
 use fs::Fs;
 use futures::{
     future::{self, Shared},
+    stream::FuturesUnordered,
     FutureExt,
 };
-use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
+use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Task, WeakModel};
 use language::{
     language_settings::{Formatter, LanguageSettings, SelectedFormatter},
-    Buffer, LanguageServerName, LocalFile,
+    Buffer, LanguageRegistry, LanguageServerName, LocalFile,
 };
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use paths::default_prettier_dir;
 use prettier::Prettier;
+use smol::stream::StreamExt;
 use util::{ResultExt, TryFutureExt};
 
-use crate::{File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId};
-
-pub fn prettier_plugins_for_language(
-    language_settings: &LanguageSettings,
-) -> Option<&HashSet<String>> {
-    match &language_settings.formatter {
-        SelectedFormatter::Auto => Some(&language_settings.prettier.plugins),
-
-        SelectedFormatter::List(list) => list
-            .as_ref()
-            .contains(&Formatter::Prettier)
-            .then_some(&language_settings.prettier.plugins),
-    }
-}
-
-pub(super) async fn format_with_prettier(
-    project: &WeakModel<Project>,
-    buffer: &Model<Buffer>,
-    cx: &mut AsyncAppContext,
-) -> Option<Result<FormatOperation>> {
-    let prettier_instance = project
-        .update(cx, |project, cx| {
-            project.prettier_instance_for_buffer(buffer, cx)
-        })
-        .ok()?
-        .await;
-
-    let (prettier_path, prettier_task) = prettier_instance?;
-
-    let prettier_description = match prettier_path.as_ref() {
-        Some(path) => format!("prettier at {path:?}"),
-        None => "default prettier instance".to_string(),
-    };
-
-    match prettier_task.await {
-        Ok(prettier) => {
-            let buffer_path = buffer
-                .update(cx, |buffer, cx| {
-                    File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
-                })
-                .ok()
-                .flatten();
-
-            let format_result = prettier
-                .format(buffer, buffer_path, cx)
-                .await
-                .map(FormatOperation::Prettier)
-                .with_context(|| format!("{} failed to format buffer", prettier_description));
-
-            Some(format_result)
-        }
-        Err(error) => {
-            project
-                .update(cx, |project, _| {
-                    let instance_to_update = match prettier_path {
-                        Some(prettier_path) => project.prettier_instances.get_mut(&prettier_path),
-                        None => match &mut project.default_prettier.prettier {
-                            PrettierInstallation::NotInstalled { .. } => None,
-                            PrettierInstallation::Installed(instance) => Some(instance),
-                        },
-                    };
-
-                    if let Some(instance) = instance_to_update {
-                        instance.attempt += 1;
-                        instance.prettier = None;
-                    }
-                })
-                .log_err();
-
-            Some(Err(anyhow!(
-                "{} failed to spawn: {error:#}",
-                prettier_description
-            )))
-        }
-    }
-}
+use crate::{
+    worktree_store::WorktreeStore, File, FormatOperation, PathChange, ProjectEntryId, Worktree,
+    WorktreeId,
+};
 
-pub struct DefaultPrettier {
-    prettier: PrettierInstallation,
-    installed_plugins: HashSet<Arc<str>>,
+pub struct PrettierStore {
+    node: Arc<dyn NodeRuntime>,
+    fs: Arc<dyn Fs>,
+    languages: Arc<LanguageRegistry>,
+    worktree_store: Model<WorktreeStore>,
+    default_prettier: DefaultPrettier,
+    prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
+    prettier_instances: HashMap<PathBuf, PrettierInstance>,
 }
 
-#[derive(Debug)]
-pub enum PrettierInstallation {
-    NotInstalled {
-        attempts: usize,
-        installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
-        not_installed_plugins: HashSet<Arc<str>>,
+pub enum PrettierStoreEvent {
+    LanguageServerRemoved(LanguageServerId),
+    LanguageServerAdded {
+        new_server_id: LanguageServerId,
+        name: LanguageServerName,
+        prettier_server: Arc<LanguageServer>,
     },
-    Installed(PrettierInstance),
 }
 
-pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
-
-#[derive(Debug, Clone)]
-pub struct PrettierInstance {
-    attempt: usize,
-    prettier: Option<PrettierTask>,
-}
+impl EventEmitter<PrettierStoreEvent> for PrettierStore {}
 
-impl Default for DefaultPrettier {
-    fn default() -> Self {
+impl PrettierStore {
+    pub fn new(
+        node: Arc<dyn NodeRuntime>,
+        fs: Arc<dyn Fs>,
+        languages: Arc<LanguageRegistry>,
+        worktree_store: Model<WorktreeStore>,
+        _: &mut ModelContext<Self>,
+    ) -> Self {
         Self {
-            prettier: PrettierInstallation::NotInstalled {
-                attempts: 0,
-                installation_task: None,
-                not_installed_plugins: HashSet::default(),
-            },
-            installed_plugins: HashSet::default(),
+            node,
+            fs,
+            languages,
+            worktree_store,
+            default_prettier: DefaultPrettier::default(),
+            prettiers_per_worktree: HashMap::default(),
+            prettier_instances: HashMap::default(),
         }
     }
-}
 
-impl DefaultPrettier {
-    pub fn instance(&self) -> Option<&PrettierInstance> {
-        if let PrettierInstallation::Installed(instance) = &self.prettier {
-            Some(instance)
-        } else {
-            None
+    pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
+        let mut prettier_instances_to_clean = FuturesUnordered::new();
+        if let Some(prettier_paths) = self.prettiers_per_worktree.remove(&id_to_remove) {
+            for path in prettier_paths.iter().flatten() {
+                if let Some(prettier_instance) = self.prettier_instances.remove(path) {
+                    prettier_instances_to_clean.push(async move {
+                        prettier_instance
+                            .server()
+                            .await
+                            .map(|server| server.server_id())
+                    });
+                }
+            }
         }
+        cx.spawn(|prettier_store, mut cx| async move {
+            while let Some(prettier_server_id) = prettier_instances_to_clean.next().await {
+                if let Some(prettier_server_id) = prettier_server_id {
+                    prettier_store
+                        .update(&mut cx, |_, cx| {
+                            cx.emit(PrettierStoreEvent::LanguageServerRemoved(
+                                prettier_server_id,
+                            ));
+                        })
+                        .ok();
+                }
+            }
+        })
+        .detach();
     }
 
-    pub fn prettier_task(
+    fn prettier_instance_for_buffer(
         &mut self,
-        node: &Arc<dyn NodeRuntime>,
-        worktree_id: Option<WorktreeId>,
-        cx: &mut ModelContext<'_, Project>,
-    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
-        match &mut self.prettier {
-            PrettierInstallation::NotInstalled { .. } => {
-                Some(start_default_prettier(Arc::clone(node), worktree_id, cx))
+        buffer: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
+        let buffer = buffer.read(cx);
+        let buffer_file = buffer.file();
+        if buffer.language().is_none() {
+            return Task::ready(None);
+        }
+
+        let node = self.node.clone();
+
+        match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) {
+            Some((worktree_id, buffer_path)) => {
+                let fs = Arc::clone(&self.fs);
+                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
+                cx.spawn(|lsp_store, mut cx| async move {
+                    match cx
+                        .background_executor()
+                        .spawn(async move {
+                            Prettier::locate_prettier_installation(
+                                fs.as_ref(),
+                                &installed_prettiers,
+                                &buffer_path,
+                            )
+                            .await
+                        })
+                        .await
+                    {
+                        Ok(ControlFlow::Break(())) => None,
+                        Ok(ControlFlow::Continue(None)) => {
+                            let default_instance = lsp_store
+                                .update(&mut cx, |lsp_store, cx| {
+                                    lsp_store
+                                        .prettiers_per_worktree
+                                        .entry(worktree_id)
+                                        .or_default()
+                                        .insert(None);
+                                    lsp_store.default_prettier.prettier_task(
+                                        &node,
+                                        Some(worktree_id),
+                                        cx,
+                                    )
+                                })
+                                .ok()?;
+                            Some((None, default_instance?.log_err().await?))
+                        }
+                        Ok(ControlFlow::Continue(Some(prettier_dir))) => {
+                            lsp_store
+                                .update(&mut cx, |lsp_store, _| {
+                                    lsp_store
+                                        .prettiers_per_worktree
+                                        .entry(worktree_id)
+                                        .or_default()
+                                        .insert(Some(prettier_dir.clone()))
+                                })
+                                .ok()?;
+                            if let Some(prettier_task) = lsp_store
+                                .update(&mut cx, |lsp_store, cx| {
+                                    lsp_store.prettier_instances.get_mut(&prettier_dir).map(
+                                        |existing_instance| {
+                                            existing_instance.prettier_task(
+                                                &node,
+                                                Some(&prettier_dir),
+                                                Some(worktree_id),
+                                                cx,
+                                            )
+                                        },
+                                    )
+                                })
+                                .ok()?
+                            {
+                                log::debug!("Found already started prettier in {prettier_dir:?}");
+                                return Some((Some(prettier_dir), prettier_task?.await.log_err()?));
+                            }
+
+                            log::info!("Found prettier in {prettier_dir:?}, starting.");
+                            let new_prettier_task = lsp_store
+                                .update(&mut cx, |lsp_store, cx| {
+                                    let new_prettier_task = Self::start_prettier(
+                                        node,
+                                        prettier_dir.clone(),
+                                        Some(worktree_id),
+                                        cx,
+                                    );
+                                    lsp_store.prettier_instances.insert(
+                                        prettier_dir.clone(),
+                                        PrettierInstance {
+                                            attempt: 0,
+                                            prettier: Some(new_prettier_task.clone()),
+                                        },
+                                    );
+                                    new_prettier_task
+                                })
+                                .ok()?;
+                            Some((Some(prettier_dir), new_prettier_task))
+                        }
+                        Err(e) => {
+                            log::error!("Failed to determine prettier path for buffer: {e:#}");
+                            None
+                        }
+                    }
+                })
             }
-            PrettierInstallation::Installed(existing_instance) => {
-                existing_instance.prettier_task(node, None, worktree_id, cx)
+            None => {
+                let new_task = self.default_prettier.prettier_task(&node, None, cx);
+                cx.spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) })
             }
         }
     }
-}
 
-impl PrettierInstance {
-    pub fn prettier_task(
-        &mut self,
-        node: &Arc<dyn NodeRuntime>,
-        prettier_dir: Option<&Path>,
+    fn start_prettier(
+        node: Arc<dyn NodeRuntime>,
+        prettier_dir: PathBuf,
         worktree_id: Option<WorktreeId>,
-        cx: &mut ModelContext<'_, Project>,
-    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
-        if self.attempt > prettier::FAIL_THRESHOLD {
-            match prettier_dir {
-                Some(prettier_dir) => log::warn!(
-                    "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
-                ),
-                None => log::warn!("Default prettier exceeded launch threshold, not starting"),
-            }
-            return None;
-        }
-        Some(match &self.prettier {
-            Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
-            None => match prettier_dir {
-                Some(prettier_dir) => {
-                    let new_task = start_prettier(
-                        Arc::clone(node),
-                        prettier_dir.to_path_buf(),
-                        worktree_id,
-                        cx,
-                    );
-                    self.attempt += 1;
-                    self.prettier = Some(new_task.clone());
-                    Task::ready(Ok(new_task))
-                }
-                None => {
-                    self.attempt += 1;
-                    let node = Arc::clone(node);
-                    cx.spawn(|project, mut cx| async move {
-                        project
-                            .update(&mut cx, |_, cx| {
-                                start_default_prettier(node, worktree_id, cx)
-                            })?
-                            .await
-                    })
-                }
-            },
+        cx: &mut ModelContext<Self>,
+    ) -> PrettierTask {
+        cx.spawn(|prettier_store, mut cx| async move {
+            log::info!("Starting prettier at path {prettier_dir:?}");
+            let new_server_id = prettier_store.update(&mut cx, |prettier_store, _| {
+                prettier_store.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)?;
+            Self::register_new_prettier(
+                &prettier_store,
+                &new_prettier,
+                worktree_id,
+                new_server_id,
+                &mut cx,
+            );
+            Ok(new_prettier)
         })
+        .shared()
     }
 
-    pub async fn server(&self) -> Option<Arc<LanguageServer>> {
-        self.prettier.clone()?.await.ok()?.server().cloned()
-    }
-}
-
-fn start_default_prettier(
-    node: Arc<dyn NodeRuntime>,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Task<anyhow::Result<PrettierTask>> {
-    cx.spawn(|project, mut cx| async move {
-        let installation_task = project.update(&mut cx, |project, _| {
-            match &project.default_prettier.prettier {
-                PrettierInstallation::NotInstalled {
-                    installation_task, ..
-                } => ControlFlow::Continue(installation_task.clone()),
-                PrettierInstallation::Installed(default_prettier) => {
-                    ControlFlow::Break(default_prettier.clone())
-                }
-            }
-        })?;
-        match installation_task {
-            ControlFlow::Continue(None) => {
-                anyhow::bail!("Default prettier is not installed and cannot be started")
-            }
-            ControlFlow::Continue(Some(installation_task)) => {
-                log::info!("Waiting for default prettier to install");
-                if let Err(e) = installation_task.await {
-                    project.update(&mut cx, |project, _| {
-                        if let PrettierInstallation::NotInstalled {
-                            installation_task,
-                            attempts,
-                            ..
-                        } = &mut project.default_prettier.prettier
-                        {
-                            *installation_task = None;
-                            *attempts += 1;
-                        }
-                    })?;
-                    anyhow::bail!(
-                        "Cannot start default prettier due to its installation failure: {e:#}"
-                    );
+    fn start_default_prettier(
+        node: Arc<dyn NodeRuntime>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<PrettierStore>,
+    ) -> Task<anyhow::Result<PrettierTask>> {
+        cx.spawn(|prettier_store, mut cx| async move {
+            let installation_task = prettier_store.update(&mut cx, |prettier_store, _| {
+                match &prettier_store.default_prettier.prettier {
+                    PrettierInstallation::NotInstalled {
+                        installation_task, ..
+                    } => ControlFlow::Continue(installation_task.clone()),
+                    PrettierInstallation::Installed(default_prettier) => {
+                        ControlFlow::Break(default_prettier.clone())
+                    }
+                }
+            })?;
+            match installation_task {
+                ControlFlow::Continue(None) => {
+                    anyhow::bail!("Default prettier is not installed and cannot be started")
                 }
-                let new_default_prettier = project.update(&mut cx, |project, cx| {
+                ControlFlow::Continue(Some(installation_task)) => {
+                    log::info!("Waiting for default prettier to install");
+                    if let Err(e) = installation_task.await {
+                        prettier_store.update(&mut cx, |project, _| {
+                            if let PrettierInstallation::NotInstalled {
+                                installation_task,
+                                attempts,
+                                ..
+                            } = &mut project.default_prettier.prettier
+                            {
+                                *installation_task = None;
+                                *attempts += 1;
+                            }
+                        })?;
+                        anyhow::bail!(
+                            "Cannot start default prettier due to its installation failure: {e:#}"
+                        );
+                    }
                     let new_default_prettier =
-                        start_prettier(node, default_prettier_dir().clone(), worktree_id, cx);
-                    project.default_prettier.prettier =
-                        PrettierInstallation::Installed(PrettierInstance {
-                            attempt: 0,
-                            prettier: Some(new_default_prettier.clone()),
-                        });
-                    new_default_prettier
-                })?;
-                Ok(new_default_prettier)
-            }
-            ControlFlow::Break(instance) => match instance.prettier {
-                Some(instance) => Ok(instance),
-                None => {
-                    let new_default_prettier = project.update(&mut cx, |project, cx| {
-                        let new_default_prettier =
-                            start_prettier(node, default_prettier_dir().clone(), worktree_id, cx);
-                        project.default_prettier.prettier =
-                            PrettierInstallation::Installed(PrettierInstance {
-                                attempt: instance.attempt + 1,
-                                prettier: Some(new_default_prettier.clone()),
-                            });
-                        new_default_prettier
-                    })?;
+                        prettier_store.update(&mut cx, |prettier_store, cx| {
+                            let new_default_prettier = Self::start_prettier(
+                                node,
+                                default_prettier_dir().clone(),
+                                worktree_id,
+                                cx,
+                            );
+                            prettier_store.default_prettier.prettier =
+                                PrettierInstallation::Installed(PrettierInstance {
+                                    attempt: 0,
+                                    prettier: Some(new_default_prettier.clone()),
+                                });
+                            new_default_prettier
+                        })?;
                     Ok(new_default_prettier)
                 }
-            },
-        }
-    })
-}
-
-fn start_prettier(
-    node: Arc<dyn NodeRuntime>,
-    prettier_dir: PathBuf,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> PrettierTask {
-    cx.spawn(|project, mut cx| async move {
-        log::info!("Starting prettier at path {prettier_dir:?}");
-        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)
-    })
-    .shared()
-}
-
-fn register_new_prettier(
-    project: &WeakModel<Project>,
-    prettier: &Prettier,
-    worktree_id: Option<WorktreeId>,
-    new_server_id: LanguageServerId,
-    cx: &mut AsyncAppContext,
-) {
-    let prettier_dir = prettier.prettier_dir();
-    let is_default = prettier.is_default();
-    if is_default {
-        log::info!("Started default prettier in {prettier_dir:?}");
-    } else {
-        log::info!("Started prettier in {prettier_dir:?}");
+                ControlFlow::Break(instance) => match instance.prettier {
+                    Some(instance) => Ok(instance),
+                    None => {
+                        let new_default_prettier =
+                            prettier_store.update(&mut cx, |prettier_store, cx| {
+                                let new_default_prettier = Self::start_prettier(
+                                    node,
+                                    default_prettier_dir().clone(),
+                                    worktree_id,
+                                    cx,
+                                );
+                                prettier_store.default_prettier.prettier =
+                                    PrettierInstallation::Installed(PrettierInstance {
+                                        attempt: instance.attempt + 1,
+                                        prettier: Some(new_default_prettier.clone()),
+                                    });
+                                new_default_prettier
+                            })?;
+                        Ok(new_default_prettier)
+                    }
+                },
+            }
+        })
     }
-    if let Some(prettier_server) = prettier.server() {
-        project
-            .update(cx, |project, cx| {
-                let name = if is_default {
-                    LanguageServerName(Arc::from("prettier (default)"))
-                } else {
-                    let worktree_path = worktree_id
-                        .and_then(|id| project.worktree_for_id(id, cx))
-                        .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
-                    let name = match worktree_path {
-                        Some(worktree_path) => {
-                            if prettier_dir == worktree_path.as_ref() {
-                                let name = prettier_dir
-                                    .file_name()
-                                    .and_then(|name| name.to_str())
-                                    .unwrap_or_default();
-                                format!("prettier ({name})")
-                            } else {
-                                let dir_to_display = prettier_dir
-                                    .strip_prefix(worktree_path.as_ref())
-                                    .ok()
-                                    .unwrap_or(prettier_dir);
-                                format!("prettier ({})", dir_to_display.display())
+
+    fn register_new_prettier(
+        prettier_store: &WeakModel<Self>,
+        prettier: &Prettier,
+        worktree_id: Option<WorktreeId>,
+        new_server_id: LanguageServerId,
+        cx: &mut AsyncAppContext,
+    ) {
+        let prettier_dir = prettier.prettier_dir();
+        let is_default = prettier.is_default();
+        if is_default {
+            log::info!("Started default prettier in {prettier_dir:?}");
+        } else {
+            log::info!("Started prettier in {prettier_dir:?}");
+        }
+        if let Some(prettier_server) = prettier.server() {
+            prettier_store
+                .update(cx, |prettier_store, cx| {
+                    let name = if is_default {
+                        LanguageServerName(Arc::from("prettier (default)"))
+                    } else {
+                        let worktree_path = worktree_id
+                            .and_then(|id| {
+                                prettier_store
+                                    .worktree_store
+                                    .read(cx)
+                                    .worktree_for_id(id, cx)
+                            })
+                            .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
+                        let name = match worktree_path {
+                            Some(worktree_path) => {
+                                if prettier_dir == worktree_path.as_ref() {
+                                    let name = prettier_dir
+                                        .file_name()
+                                        .and_then(|name| name.to_str())
+                                        .unwrap_or_default();
+                                    format!("prettier ({name})")
+                                } else {
+                                    let dir_to_display = prettier_dir
+                                        .strip_prefix(worktree_path.as_ref())
+                                        .ok()
+                                        .unwrap_or(prettier_dir);
+                                    format!("prettier ({})", dir_to_display.display())
+                                }
                             }
-                        }
-                        None => format!("prettier ({})", prettier_dir.display()),
+                            None => format!("prettier ({})", prettier_dir.display()),
+                        };
+                        LanguageServerName(Arc::from(name))
                     };
-                    LanguageServerName(Arc::from(name))
-                };
-                project.lsp_store.update(cx, |lsp_store, cx| {
-                    lsp_store.register_supplementary_language_server(
+                    cx.emit(PrettierStoreEvent::LanguageServerAdded {
                         new_server_id,
                         name,
-                        Arc::clone(prettier_server),
-                        cx,
-                    )
-                });
-            })
-            .ok();
-    }
-}
-
-async fn install_prettier_packages(
-    fs: &dyn Fs,
-    plugins_to_install: HashSet<Arc<str>>,
-    node: Arc<dyn NodeRuntime>,
-) -> anyhow::Result<()> {
-    let packages_to_versions = future::try_join_all(
-        plugins_to_install
-            .iter()
-            .chain(Some(&"prettier".into()))
-            .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 default_prettier_dir = default_prettier_dir().as_path();
-    match fs.metadata(default_prettier_dir).await.with_context(|| {
-        format!("fetching FS metadata for default prettier dir {default_prettier_dir:?}")
-    })? {
-        Some(prettier_dir_metadata) => anyhow::ensure!(
-            prettier_dir_metadata.is_dir,
-            "default prettier dir {default_prettier_dir:?} is not a directory"
-        ),
-        None => fs
-            .create_dir(default_prettier_dir)
-            .await
-            .with_context(|| format!("creating default prettier dir {default_prettier_dir:?}"))?,
-    }
-
-    log::info!("Installing default prettier and plugins: {packages_to_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(())
-}
-
-async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> {
-    let prettier_wrapper_path = default_prettier_dir().join(prettier::PRETTIER_SERVER_FILE);
-    fs.save(
-        &prettier_wrapper_path,
-        &text::Rope::from(prettier::PRETTIER_SERVER_JS),
-        text::LineEnding::Unix,
-    )
-    .await
-    .with_context(|| {
-        format!(
-            "writing {} file at {prettier_wrapper_path:?}",
-            prettier::PRETTIER_SERVER_FILE
-        )
-    })?;
-    Ok(())
-}
-
-async fn should_write_prettier_server_file(fs: &dyn Fs) -> bool {
-    let prettier_wrapper_path = default_prettier_dir().join(prettier::PRETTIER_SERVER_FILE);
-    if !fs.is_file(&prettier_wrapper_path).await {
-        return true;
+                        prettier_server: prettier_server.clone(),
+                    });
+                })
+                .ok();
+        }
     }
-    let Ok(prettier_server_file_contents) = fs.load(&prettier_wrapper_path).await else {
-        return true;
-    };
-    prettier_server_file_contents != prettier::PRETTIER_SERVER_JS
-}
 
-impl Project {
     pub fn update_prettier_settings(
         &self,
         worktree: &Model<Worktree>,
         changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
-        cx: &mut ModelContext<'_, Project>,
+        cx: &mut ModelContext<Self>,
     ) {
         let prettier_config_files = Prettier::CONFIG_FILE_NAMES
             .iter()
@@ -510,122 +450,6 @@ impl Project {
         }
     }
 
-    fn prettier_instance_for_buffer(
-        &mut self,
-        buffer: &Model<Buffer>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
-        // todo(ssh remote): prettier support
-        if self.is_via_collab() || self.ssh_session.is_some() {
-            return Task::ready(None);
-        }
-        let buffer = buffer.read(cx);
-        let buffer_file = buffer.file();
-        if buffer.language().is_none() {
-            return Task::ready(None);
-        }
-        let Some(node) = self.node.clone() else {
-            return Task::ready(None);
-        };
-        match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) {
-            Some((worktree_id, buffer_path)) => {
-                let fs = Arc::clone(&self.fs);
-                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
-                cx.spawn(|project, mut cx| async move {
-                    match cx
-                        .background_executor()
-                        .spawn(async move {
-                            Prettier::locate_prettier_installation(
-                                fs.as_ref(),
-                                &installed_prettiers,
-                                &buffer_path,
-                            )
-                            .await
-                        })
-                        .await
-                    {
-                        Ok(ControlFlow::Break(())) => None,
-                        Ok(ControlFlow::Continue(None)) => {
-                            let default_instance = project
-                                .update(&mut cx, |project, cx| {
-                                    project
-                                        .prettiers_per_worktree
-                                        .entry(worktree_id)
-                                        .or_default()
-                                        .insert(None);
-                                    project.default_prettier.prettier_task(
-                                        &node,
-                                        Some(worktree_id),
-                                        cx,
-                                    )
-                                })
-                                .ok()?;
-                            Some((None, default_instance?.log_err().await?))
-                        }
-                        Ok(ControlFlow::Continue(Some(prettier_dir))) => {
-                            project
-                                .update(&mut cx, |project, _| {
-                                    project
-                                        .prettiers_per_worktree
-                                        .entry(worktree_id)
-                                        .or_default()
-                                        .insert(Some(prettier_dir.clone()))
-                                })
-                                .ok()?;
-                            if let Some(prettier_task) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.prettier_instances.get_mut(&prettier_dir).map(
-                                        |existing_instance| {
-                                            existing_instance.prettier_task(
-                                                &node,
-                                                Some(&prettier_dir),
-                                                Some(worktree_id),
-                                                cx,
-                                            )
-                                        },
-                                    )
-                                })
-                                .ok()?
-                            {
-                                log::debug!("Found already started prettier in {prettier_dir:?}");
-                                return Some((Some(prettier_dir), prettier_task?.await.log_err()?));
-                            }
-
-                            log::info!("Found prettier in {prettier_dir:?}, starting.");
-                            let new_prettier_task = project
-                                .update(&mut cx, |project, cx| {
-                                    let new_prettier_task = start_prettier(
-                                        node,
-                                        prettier_dir.clone(),
-                                        Some(worktree_id),
-                                        cx,
-                                    );
-                                    project.prettier_instances.insert(
-                                        prettier_dir.clone(),
-                                        PrettierInstance {
-                                            attempt: 0,
-                                            prettier: Some(new_prettier_task.clone()),
-                                        },
-                                    );
-                                    new_prettier_task
-                                })
-                                .ok()?;
-                            Some((Some(prettier_dir), new_prettier_task))
-                        }
-                        Err(e) => {
-                            log::error!("Failed to determine prettier path for buffer: {e:#}");
-                            None
-                        }
-                    }
-                })
-            }
-            None => {
-                let new_task = self.default_prettier.prettier_task(&node, None, cx);
-                cx.spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) })
-            }
-        }
-    }
-
     pub fn install_default_prettier(
         &mut self,
         worktree: Option<WorktreeId>,
@@ -642,12 +466,13 @@ impl Project {
         }
 
         let mut new_plugins = plugins.collect::<HashSet<_>>();
-        let Some(node) = self.node.as_ref().cloned() else {
-            return;
-        };
+        let node = self.node.clone();
+
         let fs = Arc::clone(&self.fs);
         let locate_prettier_installation = match worktree.and_then(|worktree_id| {
-            self.worktree_for_id(worktree_id, cx)
+            self.worktree_store
+                .read(cx)
+                .worktree_for_id(worktree_id, cx)
                 .map(|worktree| worktree.read(cx).abs_path())
         }) {
             Some(locate_from) => {
@@ -777,4 +602,291 @@ impl Project {
             not_installed_plugins: plugins_to_install,
         };
     }
+
+    pub fn on_settings_changed(
+        &mut self,
+        language_formatters_to_check: Vec<(Option<WorktreeId>, LanguageSettings)>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let mut prettier_plugins_by_worktree = HashMap::default();
+        for (worktree, language_settings) in language_formatters_to_check {
+            if let Some(plugins) = prettier_plugins_for_language(&language_settings) {
+                prettier_plugins_by_worktree
+                    .entry(worktree)
+                    .or_insert_with(HashSet::default)
+                    .extend(plugins.iter().cloned());
+            }
+        }
+        for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
+            self.install_default_prettier(
+                worktree,
+                prettier_plugins.into_iter().map(Arc::from),
+                cx,
+            );
+        }
+    }
+}
+
+pub fn prettier_plugins_for_language(
+    language_settings: &LanguageSettings,
+) -> Option<&HashSet<String>> {
+    match &language_settings.formatter {
+        SelectedFormatter::Auto => Some(&language_settings.prettier.plugins),
+
+        SelectedFormatter::List(list) => list
+            .as_ref()
+            .contains(&Formatter::Prettier)
+            .then_some(&language_settings.prettier.plugins),
+    }
+}
+
+pub(super) async fn format_with_prettier(
+    prettier_store: &WeakModel<PrettierStore>,
+    buffer: &Model<Buffer>,
+    cx: &mut AsyncAppContext,
+) -> Option<Result<FormatOperation>> {
+    let prettier_instance = prettier_store
+        .update(cx, |prettier_store, cx| {
+            prettier_store.prettier_instance_for_buffer(buffer, cx)
+        })
+        .ok()?
+        .await;
+
+    let (prettier_path, prettier_task) = prettier_instance?;
+
+    let prettier_description = match prettier_path.as_ref() {
+        Some(path) => format!("prettier at {path:?}"),
+        None => "default prettier instance".to_string(),
+    };
+
+    match prettier_task.await {
+        Ok(prettier) => {
+            let buffer_path = buffer
+                .update(cx, |buffer, cx| {
+                    File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
+                })
+                .ok()
+                .flatten();
+
+            let format_result = prettier
+                .format(buffer, buffer_path, cx)
+                .await
+                .map(FormatOperation::Prettier)
+                .with_context(|| format!("{} failed to format buffer", prettier_description));
+
+            Some(format_result)
+        }
+        Err(error) => {
+            prettier_store
+                .update(cx, |project, _| {
+                    let instance_to_update = match prettier_path {
+                        Some(prettier_path) => project.prettier_instances.get_mut(&prettier_path),
+                        None => match &mut project.default_prettier.prettier {
+                            PrettierInstallation::NotInstalled { .. } => None,
+                            PrettierInstallation::Installed(instance) => Some(instance),
+                        },
+                    };
+
+                    if let Some(instance) = instance_to_update {
+                        instance.attempt += 1;
+                        instance.prettier = None;
+                    }
+                })
+                .log_err();
+
+            Some(Err(anyhow!(
+                "{} failed to spawn: {error:#}",
+                prettier_description
+            )))
+        }
+    }
+}
+
+pub struct DefaultPrettier {
+    prettier: PrettierInstallation,
+    installed_plugins: HashSet<Arc<str>>,
+}
+
+#[derive(Debug)]
+pub enum PrettierInstallation {
+    NotInstalled {
+        attempts: usize,
+        installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
+        not_installed_plugins: HashSet<Arc<str>>,
+    },
+    Installed(PrettierInstance),
+}
+
+pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
+
+#[derive(Debug, Clone)]
+pub struct PrettierInstance {
+    attempt: usize,
+    prettier: Option<PrettierTask>,
+}
+
+impl Default for DefaultPrettier {
+    fn default() -> Self {
+        Self {
+            prettier: PrettierInstallation::NotInstalled {
+                attempts: 0,
+                installation_task: None,
+                not_installed_plugins: HashSet::default(),
+            },
+            installed_plugins: HashSet::default(),
+        }
+    }
+}
+
+impl DefaultPrettier {
+    pub fn instance(&self) -> Option<&PrettierInstance> {
+        if let PrettierInstallation::Installed(instance) = &self.prettier {
+            Some(instance)
+        } else {
+            None
+        }
+    }
+
+    pub fn prettier_task(
+        &mut self,
+        node: &Arc<dyn NodeRuntime>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<PrettierStore>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        match &mut self.prettier {
+            PrettierInstallation::NotInstalled { .. } => Some(
+                PrettierStore::start_default_prettier(node.clone(), worktree_id, cx),
+            ),
+            PrettierInstallation::Installed(existing_instance) => {
+                existing_instance.prettier_task(node, None, worktree_id, cx)
+            }
+        }
+    }
+}
+
+impl PrettierInstance {
+    pub fn prettier_task(
+        &mut self,
+        node: &Arc<dyn NodeRuntime>,
+        prettier_dir: Option<&Path>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<PrettierStore>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        if self.attempt > prettier::FAIL_THRESHOLD {
+            match prettier_dir {
+                Some(prettier_dir) => log::warn!(
+                    "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
+                ),
+                None => log::warn!("Default prettier exceeded launch threshold, not starting"),
+            }
+            return None;
+        }
+        Some(match &self.prettier {
+            Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
+            None => match prettier_dir {
+                Some(prettier_dir) => {
+                    let new_task = PrettierStore::start_prettier(
+                        Arc::clone(node),
+                        prettier_dir.to_path_buf(),
+                        worktree_id,
+                        cx,
+                    );
+                    self.attempt += 1;
+                    self.prettier = Some(new_task.clone());
+                    Task::ready(Ok(new_task))
+                }
+                None => {
+                    self.attempt += 1;
+                    let node = Arc::clone(node);
+                    cx.spawn(|prettier_store, mut cx| async move {
+                        prettier_store
+                            .update(&mut cx, |_, cx| {
+                                PrettierStore::start_default_prettier(node, worktree_id, cx)
+                            })?
+                            .await
+                    })
+                }
+            },
+        })
+    }
+
+    pub async fn server(&self) -> Option<Arc<LanguageServer>> {
+        self.prettier.clone()?.await.ok()?.server().cloned()
+    }
+}
+
+async fn install_prettier_packages(
+    fs: &dyn Fs,
+    plugins_to_install: HashSet<Arc<str>>,
+    node: Arc<dyn NodeRuntime>,
+) -> anyhow::Result<()> {
+    let packages_to_versions = future::try_join_all(
+        plugins_to_install
+            .iter()
+            .chain(Some(&"prettier".into()))
+            .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 default_prettier_dir = default_prettier_dir().as_path();
+    match fs.metadata(default_prettier_dir).await.with_context(|| {
+        format!("fetching FS metadata for default prettier dir {default_prettier_dir:?}")
+    })? {
+        Some(prettier_dir_metadata) => anyhow::ensure!(
+            prettier_dir_metadata.is_dir,
+            "default prettier dir {default_prettier_dir:?} is not a directory"
+        ),
+        None => fs
+            .create_dir(default_prettier_dir)
+            .await
+            .with_context(|| format!("creating default prettier dir {default_prettier_dir:?}"))?,
+    }
+
+    log::info!("Installing default prettier and plugins: {packages_to_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(())
+}
+
+async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> {
+    let prettier_wrapper_path = default_prettier_dir().join(prettier::PRETTIER_SERVER_FILE);
+    fs.save(
+        &prettier_wrapper_path,
+        &text::Rope::from(prettier::PRETTIER_SERVER_JS),
+        text::LineEnding::Unix,
+    )
+    .await
+    .with_context(|| {
+        format!(
+            "writing {} file at {prettier_wrapper_path:?}",
+            prettier::PRETTIER_SERVER_FILE
+        )
+    })?;
+    Ok(())
+}
+
+async fn should_write_prettier_server_file(fs: &dyn Fs) -> bool {
+    let prettier_wrapper_path = default_prettier_dir().join(prettier::PRETTIER_SERVER_FILE);
+    if !fs.is_file(&prettier_wrapper_path).await {
+        return true;
+    }
+    let Ok(prettier_server_file_contents) = fs.load(&prettier_wrapper_path).await else {
+        return true;
+    };
+    prettier_server_file_contents != prettier::PRETTIER_SERVER_JS
 }

crates/project/src/project.rs 🔗

@@ -4,7 +4,7 @@ pub mod debounced_delay;
 pub mod lsp_command;
 pub mod lsp_ext_command;
 pub mod lsp_store;
-mod prettier_support;
+pub mod prettier_store;
 pub mod project_settings;
 pub mod search;
 mod task_inventory;
@@ -31,7 +31,6 @@ pub use environment::ProjectEnvironment;
 use futures::{
     channel::mpsc::{self, UnboundedReceiver},
     future::try_join_all,
-    stream::FuturesUnordered,
     AsyncWriteExt, FutureExt, StreamExt,
 };
 
@@ -59,8 +58,8 @@ use lsp_command::*;
 use node_runtime::NodeRuntime;
 use parking_lot::{Mutex, RwLock};
 use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path};
-use prettier_support::{DefaultPrettier, PrettierInstance};
-use project_settings::{LspSettings, ProjectSettings, SettingsObserver};
+pub use prettier_store::PrettierStore;
+use project_settings::{ProjectSettings, SettingsObserver};
 use remote::SshSession;
 use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
 use search::{SearchInputKind, SearchQuery, SearchResult};
@@ -140,7 +139,6 @@ pub struct Project {
     buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
     languages: Arc<LanguageRegistry>,
     client: Arc<client::Client>,
-    current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     join_project_response_message_id: u32,
     user_store: Model<UserStore>,
     fs: Arc<dyn Fs>,
@@ -157,9 +155,6 @@ pub struct Project {
     remotely_created_buffers: Arc<Mutex<RemotelyCreatedBuffers>>,
     terminals: Terminals,
     node: Option<Arc<dyn NodeRuntime>>,
-    default_prettier: DefaultPrettier,
-    prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
-    prettier_instances: HashMap<PathBuf, PrettierInstance>,
     tasks: Model<Inventory>,
     hosted_project_id: Option<ProjectId>,
     dev_server_project_id: Option<client::DevServerProjectId>,
@@ -634,6 +629,16 @@ impl Project {
             cx.subscribe(&buffer_store, Self::on_buffer_store_event)
                 .detach();
 
+            let prettier_store = cx.new_model(|cx| {
+                PrettierStore::new(
+                    node.clone(),
+                    fs.clone(),
+                    languages.clone(),
+                    worktree_store.clone(),
+                    cx,
+                )
+            });
+
             let settings_observer = cx.new_model(|cx| {
                 SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx)
             });
@@ -643,6 +648,7 @@ impl Project {
                 LspStore::new_local(
                     buffer_store.clone(),
                     worktree_store.clone(),
+                    prettier_store.clone(),
                     environment.clone(),
                     languages.clone(),
                     Some(client.http_client()),
@@ -658,14 +664,10 @@ impl Project {
                 worktree_store,
                 buffer_store,
                 lsp_store,
-                current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
                 join_project_response_message_id: 0,
                 client_state: ProjectClientState::Local,
                 client_subscriptions: Vec::new(),
-                _subscriptions: vec![
-                    cx.observe_global::<SettingsStore>(Self::on_settings_changed),
-                    cx.on_release(Self::release),
-                ],
+                _subscriptions: vec![cx.on_release(Self::release)],
                 active_entry: None,
                 snippets,
                 languages,
@@ -680,9 +682,6 @@ impl Project {
                     local_handles: Vec::new(),
                 },
                 node: Some(node),
-                default_prettier: DefaultPrettier::default(),
-                prettiers_per_worktree: HashMap::default(),
-                prettier_instances: HashMap::default(),
                 tasks,
                 hosted_project_id: None,
                 dev_server_project_id: None,
@@ -751,14 +750,10 @@ impl Project {
                 worktree_store,
                 buffer_store,
                 lsp_store,
-                current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
                 join_project_response_message_id: 0,
                 client_state: ProjectClientState::Local,
                 client_subscriptions: Vec::new(),
-                _subscriptions: vec![
-                    cx.observe_global::<SettingsStore>(Self::on_settings_changed),
-                    cx.on_release(Self::release),
-                ],
+                _subscriptions: vec![cx.on_release(Self::release)],
                 active_entry: None,
                 snippets,
                 languages,
@@ -773,9 +768,6 @@ impl Project {
                     local_handles: Vec::new(),
                 },
                 node: Some(node),
-                default_prettier: DefaultPrettier::default(),
-                prettiers_per_worktree: HashMap::default(),
-                prettier_instances: HashMap::default(),
                 tasks,
                 hosted_project_id: None,
                 dev_server_project_id: None,
@@ -928,7 +920,6 @@ impl Project {
                 buffer_store: buffer_store.clone(),
                 worktree_store: worktree_store.clone(),
                 lsp_store: lsp_store.clone(),
-                current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
                 active_entry: None,
                 collaborators: Default::default(),
                 join_project_response_message_id: response.message_id,
@@ -954,9 +945,6 @@ impl Project {
                     local_handles: Vec::new(),
                 },
                 node: None,
-                default_prettier: DefaultPrettier::default(),
-                prettiers_per_worktree: HashMap::default(),
-                prettier_instances: HashMap::default(),
                 tasks,
                 hosted_project_id: None,
                 dev_server_project_id: response
@@ -1176,112 +1164,6 @@ impl Project {
         self.worktree_store.clone()
     }
 
-    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.buffer_store.read(cx).buffers() {
-            let buffer = buffer.read(cx);
-            let buffer_file = File::from_dyn(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) = buffer_file {
-                        language_servers_to_start.push((file.worktree.clone(), language.name()));
-                    }
-                }
-                language_formatters_to_check
-                    .push((buffer_file.map(|f| f.worktree_id(cx)), settings.clone()));
-            }
-        }
-
-        let mut language_servers_to_stop = Vec::new();
-        let mut language_servers_to_restart = Vec::new();
-        let languages = self.languages.to_vec();
-
-        let new_lsp_settings = ProjectSettings::get_global(cx).lsp.clone();
-        let current_lsp_settings = &self.current_lsp_settings;
-        for (worktree_id, started_lsp_name) in self.lsp_store.read(cx).started_language_servers() {
-            let language = languages.iter().find_map(|l| {
-                let adapter = self
-                    .languages
-                    .lsp_adapters(&l.name())
-                    .iter()
-                    .find(|adapter| adapter.name == started_lsp_name)?
-                    .clone();
-                Some((l, adapter))
-            });
-            if let Some((language, adapter)) = language {
-                let worktree = self.worktree_for_id(worktree_id, cx);
-                let file = worktree.as_ref().and_then(|tree| {
-                    tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
-                });
-                if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
-                    language_servers_to_stop.push((worktree_id, started_lsp_name.clone()));
-                } else if let Some(worktree) = worktree {
-                    let server_name = &adapter.name.0;
-                    match (
-                        current_lsp_settings.get(server_name),
-                        new_lsp_settings.get(server_name),
-                    ) {
-                        (None, None) => {}
-                        (Some(_), None) | (None, Some(_)) => {
-                            language_servers_to_restart.push((worktree, language.name()));
-                        }
-                        (Some(current_lsp_settings), Some(new_lsp_settings)) => {
-                            if current_lsp_settings != new_lsp_settings {
-                                language_servers_to_restart.push((worktree, language.name()));
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        self.current_lsp_settings = new_lsp_settings;
-
-        // Stop all newly-disabled language servers.
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            for (worktree_id, adapter_name) in language_servers_to_stop {
-                lsp_store
-                    .stop_language_server(worktree_id, adapter_name, cx)
-                    .detach();
-            }
-        });
-
-        let mut prettier_plugins_by_worktree = HashMap::default();
-        for (worktree, language_settings) in language_formatters_to_check {
-            if let Some(plugins) =
-                prettier_support::prettier_plugins_for_language(&language_settings)
-            {
-                prettier_plugins_by_worktree
-                    .entry(worktree)
-                    .or_insert_with(HashSet::default)
-                    .extend(plugins.iter().cloned());
-            }
-        }
-        for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
-            self.install_default_prettier(
-                worktree,
-                prettier_plugins.into_iter().map(Arc::from),
-                cx,
-            );
-        }
-
-        // Start all the newly-enabled language servers.
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            for (worktree, language) in language_servers_to_start {
-                lsp_store.start_language_servers(&worktree, language, cx);
-            }
-
-            // Restart all language servers with changed initialization options.
-            for (worktree, language) in language_servers_to_restart {
-                lsp_store.restart_language_servers(worktree, language, cx);
-            }
-        });
-
-        cx.notify();
-    }
-
     pub fn buffer_for_id(&self, remote_id: BufferId, cx: &AppContext) -> Option<Model<Buffer>> {
         self.buffer_store.read(cx).get(remote_id)
     }
@@ -2160,24 +2042,10 @@ impl Project {
                 buffer,
                 new_language,
             } => {
-                let Some(new_language) = new_language else {
+                let Some(_) = new_language else {
                     cx.emit(Event::LanguageNotFound(buffer.clone()));
                     return;
                 };
-                let buffer_file = buffer.read(cx).file().cloned();
-                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(&settings)
-                {
-                    self.install_default_prettier(
-                        worktree,
-                        prettier_plugins.iter().map(|s| Arc::from(s.as_str())),
-                        cx,
-                    );
-                };
             }
             LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints),
             LspStoreEvent::LanguageServerPrompt(prompt) => {
@@ -2253,7 +2121,6 @@ impl Project {
                 worktree::Event::UpdatedEntries(changes) => {
                     if is_local {
                         this.update_local_worktree_settings(&worktree, changes, cx);
-                        this.update_prettier_settings(&worktree, changes, cx);
                     }
 
                     cx.emit(Event::WorktreeUpdatedEntries(
@@ -2300,37 +2167,6 @@ impl Project {
             return;
         }
 
-        let mut prettier_instances_to_clean = FuturesUnordered::new();
-        if let Some(prettier_paths) = self.prettiers_per_worktree.remove(&id_to_remove) {
-            for path in prettier_paths.iter().flatten() {
-                if let Some(prettier_instance) = self.prettier_instances.remove(path) {
-                    prettier_instances_to_clean.push(async move {
-                        prettier_instance
-                            .server()
-                            .await
-                            .map(|server| server.server_id())
-                    });
-                }
-            }
-        }
-        cx.spawn(|project, mut cx| async move {
-            while let Some(prettier_server_id) = prettier_instances_to_clean.next().await {
-                if let Some(prettier_server_id) = prettier_server_id {
-                    project
-                        .update(&mut cx, |project, cx| {
-                            project.lsp_store.update(cx, |lsp_store, cx| {
-                                lsp_store.unregister_supplementary_language_server(
-                                    prettier_server_id,
-                                    cx,
-                                );
-                            });
-                        })
-                        .ok();
-                }
-            }
-        })
-        .detach();
-
         self.task_inventory().update(cx, |inventory, _| {
             inventory.remove_worktree_sources(id_to_remove);
         });
@@ -3059,11 +2895,21 @@ impl Project {
                     None
                 }
             }
-            Formatter::Prettier => prettier_support::format_with_prettier(&project, buffer, cx)
-                .await
-                .transpose()
-                .ok()
-                .flatten(),
+            Formatter::Prettier => {
+                let prettier = project.update(cx, |project, cx| {
+                    project
+                        .lsp_store
+                        .read(cx)
+                        .prettier_store()
+                        .unwrap()
+                        .downgrade()
+                })?;
+                prettier_store::format_with_prettier(&prettier, buffer, 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, cx)

crates/remote_server/Cargo.toml 🔗

@@ -26,6 +26,7 @@ env_logger.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
+node_runtime.workspace = true
 log.workspace = true
 project.workspace = true
 remote.workspace = true

crates/remote_server/src/headless_project.rs 🔗

@@ -2,12 +2,13 @@ use anyhow::{anyhow, Result};
 use fs::Fs;
 use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
 use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
+use node_runtime::DummyNodeRuntime;
 use project::{
     buffer_store::{BufferStore, BufferStoreEvent},
     project_settings::SettingsObserver,
     search::SearchQuery,
     worktree_store::WorktreeStore,
-    LspStore, LspStoreEvent, ProjectPath, WorktreeId,
+    LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId,
 };
 use remote::SshSession;
 use rpc::{
@@ -54,6 +55,16 @@ impl HeadlessProject {
             buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
             buffer_store
         });
+        let prettier_store = cx.new_model(|cx| {
+            PrettierStore::new(
+                DummyNodeRuntime::new(),
+                fs.clone(),
+                languages.clone(),
+                worktree_store.clone(),
+                cx,
+            )
+        });
+
         let settings_observer = cx.new_model(|cx| {
             let mut observer = SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx);
             observer.shared(SSH_PROJECT_ID, session.clone().into(), cx);
@@ -64,6 +75,7 @@ impl HeadlessProject {
             let mut lsp_store = LspStore::new_local(
                 buffer_store.clone(),
                 worktree_store.clone(),
+                prettier_store.clone(),
                 environment,
                 languages.clone(),
                 None,