From 8e45bf71cae37cbbc99dda67fdce57aed4d2d8fd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 17 Sep 2024 16:37:56 -0600 Subject: [PATCH] Refactor prettier (#17977) In preparation for making formatting work on ssh remotes Release Notes: - N/A Co-authored-by: Mikayla --- crates/node_runtime/src/node_runtime.rs | 49 + crates/project/src/lsp_store.rs | 194 ++- ...{prettier_support.rs => prettier_store.rs} | 1140 +++++++++-------- crates/project/src/project.rs | 218 +--- crates/remote_server/Cargo.toml | 1 + crates/remote_server/src/headless_project.rs | 14 +- 6 files changed, 911 insertions(+), 705 deletions(-) rename crates/project/src/{prettier_support.rs => prettier_store.rs} (65%) diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 73650d73c9..4aa65ab6db 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/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 { + Arc::new(Self) + } +} + +#[async_trait::async_trait] +impl NodeRuntime for DummyNodeRuntime { + async fn binary_path(&self) -> anyhow::Result { + anyhow::bail!("Dummy Node Runtime") + } + + async fn node_environment_path(&self) -> anyhow::Result { + anyhow::bail!("Dummy node runtime") + } + + async fn run_npm_subcommand( + &self, + _: Option<&Path>, + _subcommand: &str, + _args: &[&str], + ) -> anyhow::Result { + anyhow::bail!("Dummy node runtime") + } + + async fn npm_package_latest_version(&self, _name: &str) -> anyhow::Result { + anyhow::bail!("Dummy node runtime") + } + + async fn npm_package_installed_version( + &self, + _local_package_directory: &Path, + _name: &str, + ) -> Result> { + anyhow::bail!("Dummy node runtime") + } + + async fn npm_install_packages( + &self, + _: &Path, + _packages: &[(&str, &str)], + ) -> anyhow::Result<()> { + anyhow::bail!("Dummy node runtime") + } +} diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index daacf26c3a..35eb20259c 100644 --- a/crates/project/src/lsp_store.rs +++ b/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>>, supplementary_language_servers: HashMap)>, + prettier_store: Model, + current_lsp_settings: HashMap, LspSettings>, _subscription: gpui::Subscription, } @@ -135,6 +138,7 @@ impl RemoteLspStore {} pub struct SshLspStore { upstream_client: AnyProtoClient, + current_lsp_settings: HashMap, LspSettings>, } #[allow(clippy::large_enum_variant)] @@ -310,9 +314,32 @@ impl LspStore { } } + pub fn swap_current_lsp_settings( + &mut self, + new_settings: HashMap, LspSettings>, + ) -> Option, 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, worktree_store: Model, + prettier_store: Model, environment: Model, languages: Arc, http_client: Option>, @@ -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::(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::(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, + event: &PrettierStoreEvent, + cx: &mut ModelContext, + ) { + 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> { + self.as_local().map(|local| local.prettier_store.clone()) + } + fn on_buffer_event( &mut self, buffer: Model, @@ -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) { + 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, adapters_and_servers: &[(Arc, Arc)], @@ -2375,7 +2551,7 @@ impl LspStore { }) } - pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { + fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { 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 diff --git a/crates/project/src/prettier_support.rs b/crates/project/src/prettier_store.rs similarity index 65% rename from crates/project/src/prettier_support.rs rename to crates/project/src/prettier_store.rs index e90a1dbdf7..29101917fb 100644 --- a/crates/project/src/prettier_support.rs +++ b/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}; +use crate::{ + worktree_store::WorktreeStore, File, FormatOperation, PathChange, ProjectEntryId, Worktree, + WorktreeId, +}; -pub fn prettier_plugins_for_language( - language_settings: &LanguageSettings, -) -> Option<&HashSet> { - 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 struct PrettierStore { + node: Arc, + fs: Arc, + languages: Arc, + worktree_store: Model, + default_prettier: DefaultPrettier, + prettiers_per_worktree: HashMap>>, + prettier_instances: HashMap, } -pub(super) async fn format_with_prettier( - project: &WeakModel, - buffer: &Model, - cx: &mut AsyncAppContext, -) -> Option> { - let prettier_instance = project - .update(cx, |project, cx| { - project.prettier_instance_for_buffer(buffer, cx) - }) - .ok()? - .await; +pub enum PrettierStoreEvent { + LanguageServerRemoved(LanguageServerId), + LanguageServerAdded { + new_server_id: LanguageServerId, + name: LanguageServerName, + prettier_server: Arc, + }, +} - let (prettier_path, prettier_task) = prettier_instance?; +impl EventEmitter for PrettierStore {} - 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) +impl PrettierStore { + pub fn new( + node: Arc, + fs: Arc, + languages: Arc, + worktree_store: Model, + _: &mut ModelContext, + ) -> Self { + Self { + node, + fs, + languages, + worktree_store, + default_prettier: DefaultPrettier::default(), + prettiers_per_worktree: HashMap::default(), + prettier_instances: HashMap::default(), } - 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; + pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { + 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(); + } + + fn prettier_instance_for_buffer( + &mut self, + buffer: &Model, + cx: &mut ModelContext, + ) -> Task, 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 + } } }) - .log_err(); - - Some(Err(anyhow!( - "{} failed to spawn: {error:#}", - prettier_description - ))) - } - } -} - -pub struct DefaultPrettier { - prettier: PrettierInstallation, - installed_plugins: HashSet>, -} - -#[derive(Debug)] -pub enum PrettierInstallation { - NotInstalled { - attempts: usize, - installation_task: Option>>>>, - not_installed_plugins: HashSet>, - }, - Installed(PrettierInstance), -} - -pub type PrettierTask = Shared, Arc>>>; - -#[derive(Debug, Clone)] -pub struct PrettierInstance { - attempt: usize, - prettier: Option, -} - -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 + } + 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 prettier_task( - &mut self, - node: &Arc, + fn start_prettier( + node: Arc, + prettier_dir: PathBuf, worktree_id: Option, - cx: &mut ModelContext<'_, Project>, - ) -> Option>> { - match &mut self.prettier { - PrettierInstallation::NotInstalled { .. } => { - Some(start_default_prettier(Arc::clone(node), worktree_id, cx)) - } - PrettierInstallation::Installed(existing_instance) => { - existing_instance.prettier_task(node, None, worktree_id, cx) - } - } - } -} + cx: &mut ModelContext, + ) -> 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() + })?; -impl PrettierInstance { - pub fn prettier_task( - &mut self, - node: &Arc, - prettier_dir: Option<&Path>, + 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() + } + + fn start_default_prettier( + node: Arc, worktree_id: Option, - cx: &mut ModelContext<'_, Project>, - ) -> Option>> { - 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"), + cx: &mut ModelContext, + ) -> Task> { + 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") + } + 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 = + 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) + } + 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) + } + }, } - 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 - }) - } - }, }) } - pub async fn server(&self) -> Option> { - self.prettier.clone()?.await.ok()?.server().cloned() - } -} - -fn start_default_prettier( - node: Arc, - worktree_id: Option, - cx: &mut ModelContext<'_, Project>, -) -> Task> { - 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:#}" - ); - } - 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: 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 - })?; - Ok(new_default_prettier) - } - }, + fn register_new_prettier( + prettier_store: &WeakModel, + prettier: &Prettier, + worktree_id: Option, + 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:?}"); } - }) -} - -fn start_prettier( - node: Arc, - prettier_dir: PathBuf, - worktree_id: Option, - 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, - prettier: &Prettier, - worktree_id: Option, - 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() { - 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()) + 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>, - node: Arc, -) -> 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:?}"))?, + prettier_server: prettier_server.clone(), + }); + }) + .ok(); + } } - 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::>(); - 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 -} - -impl Project { pub fn update_prettier_settings( &self, worktree: &Model, changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut ModelContext<'_, Project>, + cx: &mut ModelContext, ) { let prettier_config_files = Prettier::CONFIG_FILE_NAMES .iter() @@ -510,122 +450,6 @@ impl Project { } } - fn prettier_instance_for_buffer( - &mut self, - buffer: &Model, - cx: &mut ModelContext, - ) -> Task, 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, @@ -642,12 +466,13 @@ impl Project { } let mut new_plugins = plugins.collect::>(); - 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, LanguageSettings)>, + cx: &mut ModelContext, + ) { + 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> { + 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, + buffer: &Model, + cx: &mut AsyncAppContext, +) -> Option> { + 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>, +} + +#[derive(Debug)] +pub enum PrettierInstallation { + NotInstalled { + attempts: usize, + installation_task: Option>>>>, + not_installed_plugins: HashSet>, + }, + Installed(PrettierInstance), +} + +pub type PrettierTask = Shared, Arc>>>; + +#[derive(Debug, Clone)] +pub struct PrettierInstance { + attempt: usize, + prettier: Option, +} + +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, + worktree_id: Option, + cx: &mut ModelContext, + ) -> Option>> { + 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, + prettier_dir: Option<&Path>, + worktree_id: Option, + cx: &mut ModelContext, + ) -> Option>> { + 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> { + self.prettier.clone()?.await.ok()?.server().cloned() + } +} + +async fn install_prettier_packages( + fs: &dyn Fs, + plugins_to_install: HashSet>, + node: Arc, +) -> 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::>(); + 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 } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4318737e38..f4816cf0cd 100644 --- a/crates/project/src/project.rs +++ b/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, languages: Arc, client: Arc, - current_lsp_settings: HashMap, LspSettings>, join_project_response_message_id: u32, user_store: Model, fs: Arc, @@ -157,9 +155,6 @@ pub struct Project { remotely_created_buffers: Arc>, terminals: Terminals, node: Option>, - default_prettier: DefaultPrettier, - prettiers_per_worktree: HashMap>>, - prettier_instances: HashMap, tasks: Model, hosted_project_id: Option, dev_server_project_id: Option, @@ -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::(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::(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) { - 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> { 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) diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index f5efa21bd0..ed12b41167 100644 --- a/crates/remote_server/Cargo.toml +++ b/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 diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 35d6630c1e..ec26bddfc3 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/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,