Allow extensions to define more of the methods in the LspAdapter trait (#9554)

Max Brunsfeld , Marshall , and Marshall Bowers created

Our goal is to extract Svelte support into an extension, since we've
seen problems with the Tree-sitter Svelte parser crashing due to bugs in
the external scanner. In order to do this, we need a couple more
capabilities in LSP extensions:

* [x] `initialization_options` - programmatically controlling the JSON
initialization params sent to the language server
* [x] `prettier_plugins` - statically specifying a list of prettier
plugins that apply for a given language.
* [x] `npm_install_package`

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

crates/extension/src/extension_lsp_adapter.rs | 33 ++++++++
crates/extension/src/wasm_host.rs             | 40 ++++++++++
crates/extension_api/src/extension_api.rs     | 15 +++
crates/extension_api/wit/extension.wit        |  7 +
crates/language/src/language.rs               | 34 ++++----
crates/language/src/language_registry.rs      | 38 +++++++--
crates/languages/src/astro.rs                 | 13 +-
crates/languages/src/astro/config.toml        |  1 
crates/languages/src/css.rs                   |  9 +
crates/languages/src/deno.rs                  |  9 +
crates/languages/src/go.rs                    |  9 +
crates/languages/src/html.rs                  |  9 +
crates/languages/src/json.rs                  |  9 +
crates/languages/src/php.rs                   | 15 ---
crates/languages/src/php/config.toml          |  1 
crates/languages/src/prisma.rs                |  4 -
crates/languages/src/purescript.rs            |  9 +
crates/languages/src/svelte.rs                | 13 +-
crates/languages/src/svelte/config.toml       |  1 
crates/languages/src/tailwind.rs              | 13 +-
crates/languages/src/typescript.rs            | 21 +---
crates/languages/src/vue.rs                   | 10 +-
crates/node_runtime/src/node_runtime.rs       | 79 +++++++++++++------
crates/prettier/src/prettier.rs               | 26 +++---
crates/project/src/prettier_support.rs        | 83 ++++++++------------
crates/project/src/project.rs                 | 25 ++---
26 files changed, 318 insertions(+), 208 deletions(-)

Detailed changes

crates/extension/src/extension_lsp_adapter.rs 🔗

@@ -92,4 +92,37 @@ impl LspAdapter for ExtensionLspAdapter {
     async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
         None
     }
+
+    async fn initialization_options(
+        self: Arc<Self>,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        let delegate = delegate.clone();
+        let json_options = self
+            .extension
+            .call({
+                let this = self.clone();
+                |extension, store| {
+                    async move {
+                        let resource = store.data_mut().table().push(delegate)?;
+                        let options = extension
+                            .call_language_server_initialization_options(
+                                store,
+                                &this.config,
+                                resource,
+                            )
+                            .await?
+                            .map_err(|e| anyhow!("{}", e))?;
+                        anyhow::Ok(options)
+                    }
+                    .boxed()
+                }
+            })
+            .await?;
+        Ok(if let Some(json_options) = json_options {
+            serde_json::from_str(&json_options)?
+        } else {
+            None
+        })
+    }
 }

crates/extension/src/wasm_host.rs 🔗

@@ -307,6 +307,46 @@ impl wit::ExtensionImports for WasmState {
             .map_err(|err| err.to_string()))
     }
 
+    async fn npm_package_installed_version(
+        &mut self,
+        package_name: String,
+    ) -> wasmtime::Result<Result<Option<String>, String>> {
+        async fn inner(
+            this: &mut WasmState,
+            package_name: String,
+        ) -> anyhow::Result<Option<String>> {
+            this.host
+                .node_runtime
+                .npm_package_installed_version(&this.host.work_dir, &package_name)
+                .await
+        }
+
+        Ok(inner(self, package_name)
+            .await
+            .map_err(|err| err.to_string()))
+    }
+
+    async fn npm_install_package(
+        &mut self,
+        package_name: String,
+        version: String,
+    ) -> wasmtime::Result<Result<(), String>> {
+        async fn inner(
+            this: &mut WasmState,
+            package_name: String,
+            version: String,
+        ) -> anyhow::Result<()> {
+            this.host
+                .node_runtime
+                .npm_install_packages(&this.host.work_dir, &[(&package_name, &version)])
+                .await
+        }
+
+        Ok(inner(self, package_name, version)
+            .await
+            .map_err(|err| err.to_string()))
+    }
+
     async fn latest_github_release(
         &mut self,
         repo: String,

crates/extension_api/src/extension_api.rs 🔗

@@ -13,6 +13,14 @@ pub trait Extension: Send + Sync {
         config: wit::LanguageServerConfig,
         worktree: &wit::Worktree,
     ) -> Result<Command>;
+
+    fn language_server_initialization_options(
+        &mut self,
+        _config: wit::LanguageServerConfig,
+        _worktree: &wit::Worktree,
+    ) -> Result<Option<String>> {
+        Ok(None)
+    }
 }
 
 #[macro_export]
@@ -60,4 +68,11 @@ impl wit::Guest for Component {
     ) -> Result<wit::Command> {
         extension().language_server_command(config, worktree)
     }
+
+    fn language_server_initialization_options(
+        config: LanguageServerConfig,
+        worktree: &Worktree,
+    ) -> Result<Option<String>, String> {
+        extension().language_server_initialization_options(config, worktree)
+    }
 }

crates/extension_api/wit/extension.wit 🔗

@@ -51,6 +51,12 @@ world extension {
     /// Gets the latest version of the given NPM package.
     import npm-package-latest-version: func(package-name: string) -> result<string, string>;
 
+    /// Returns the installed version of the given NPM package, if it exists.
+    import npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
+
+    /// Installs the specified NPM package.
+    import npm-install-package: func(package-name: string, version: string) -> result<_, string>;
+
     /// Gets the latest release for the given GitHub repository.
     import latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
 
@@ -81,4 +87,5 @@ world extension {
     }
 
     export language-server-command: func(config: language-server-config, worktree: borrow<worktree>) -> result<command, string>;
+    export language-server-initialization-options: func(config: language-server-config, worktree: borrow<worktree>) -> result<option<string>, string>;
 }

crates/language/src/language.rs 🔗

@@ -259,10 +259,6 @@ impl CachedLspAdapter {
         self.adapter.label_for_symbol(name, kind, language).await
     }
 
-    pub fn prettier_plugins(&self) -> &[&'static str] {
-        self.adapter.prettier_plugins()
-    }
-
     #[cfg(any(test, feature = "test-support"))]
     fn as_fake(&self) -> Option<&FakeLspAdapter> {
         self.adapter.as_fake()
@@ -441,8 +437,11 @@ pub trait LspAdapter: 'static + Send + Sync {
     }
 
     /// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp::InitializeParams`]
-    fn initialization_options(&self) -> Option<Value> {
-        None
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<Value>> {
+        Ok(None)
     }
 
     fn workspace_configuration(&self, _workspace_root: &Path, _cx: &mut AppContext) -> Value {
@@ -472,10 +471,6 @@ pub trait LspAdapter: 'static + Send + Sync {
         Default::default()
     }
 
-    fn prettier_plugins(&self) -> &[&'static str] {
-        &[]
-    }
-
     #[cfg(any(test, feature = "test-support"))]
     fn as_fake(&self) -> Option<&FakeLspAdapter> {
         None
@@ -575,6 +570,9 @@ pub struct LanguageConfig {
     /// The name of a Prettier parser that should be used for this language.
     #[serde(default)]
     pub prettier_parser_name: Option<String>,
+    /// The names of any Prettier plugins that should be used for this language.
+    #[serde(default)]
+    pub prettier_plugins: Vec<Arc<str>>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@@ -656,6 +654,7 @@ impl Default for LanguageConfig {
             overrides: Default::default(),
             word_characters: Default::default(),
             prettier_parser_name: None,
+            prettier_plugins: Default::default(),
             collapsed_placeholder: Default::default(),
         }
     }
@@ -1283,6 +1282,10 @@ impl Language {
     pub fn prettier_parser_name(&self) -> Option<&str> {
         self.config.prettier_parser_name.as_deref()
     }
+
+    pub fn prettier_plugins(&self) -> &Vec<Arc<str>> {
+        &self.config.prettier_plugins
+    }
 }
 
 impl LanguageScope {
@@ -1547,12 +1550,11 @@ impl LspAdapter for FakeLspAdapter {
         self.disk_based_diagnostics_progress_token.clone()
     }
 
-    fn initialization_options(&self) -> Option<Value> {
-        self.initialization_options.clone()
-    }
-
-    fn prettier_plugins(&self) -> &[&'static str] {
-        &self.prettier_plugins
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<Value>> {
+        Ok(self.initialization_options.clone())
     }
 
     fn as_fake(&self) -> Option<&FakeLspAdapter> {

crates/language/src/language_registry.rs 🔗

@@ -63,7 +63,7 @@ pub enum LanguageServerBinaryStatus {
 
 pub struct PendingLanguageServer {
     pub server_id: LanguageServerId,
-    pub task: Task<Result<lsp::LanguageServer>>,
+    pub task: Task<Result<(lsp::LanguageServer, Option<serde_json::Value>)>>,
     pub container_dir: Option<Arc<Path>>,
 }
 
@@ -629,6 +629,15 @@ impl LanguageRegistry {
             .unwrap_or_default()
     }
 
+    pub fn all_prettier_plugins(&self) -> Vec<Arc<str>> {
+        let state = self.state.read();
+        state
+            .languages
+            .iter()
+            .flat_map(|language| language.config.prettier_plugins.iter().cloned())
+            .collect()
+    }
+
     pub fn update_lsp_status(
         &self,
         server_name: LanguageServerName,
@@ -680,6 +689,12 @@ impl LanguageRegistry {
                     )
                     .await?;
 
+                let options = adapter
+                    .adapter
+                    .clone()
+                    .initialization_options(&delegate)
+                    .await?;
+
                 if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
                     task.await?;
                 }
@@ -727,18 +742,21 @@ impl LanguageRegistry {
                         })
                         .detach();
 
-                    return Ok(server);
+                    return Ok((server, options));
                 }
 
                 drop(this);
-                lsp::LanguageServer::new(
-                    stderr_capture,
-                    server_id,
-                    binary,
-                    &root_path,
-                    adapter.code_action_kinds(),
-                    cx,
-                )
+                Ok((
+                    lsp::LanguageServer::new(
+                        stderr_capture,
+                        server_id,
+                        binary,
+                        &root_path,
+                        adapter.code_action_kinds(),
+                        cx,
+                    )?,
+                    options,
+                ))
             }
         });
 

crates/languages/src/astro.rs 🔗

@@ -90,17 +90,16 @@ impl LspAdapter for AstroLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "provideFormatter": true,
             "typescript": {
                 "tsdk": "node_modules/typescript/lib",
             }
-        }))
-    }
-
-    fn prettier_plugins(&self) -> &[&'static str] {
-        &["prettier-plugin-astro"]
+        })))
     }
 }
 

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

@@ -16,6 +16,7 @@ brackets = [
 word_characters = ["#", "$", "-"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 prettier_parser_name = "astro"
+prettier_plugins = ["prettier-plugin-astro"]
 
 [overrides.string]
 word_characters = ["-"]

crates/languages/src/css.rs 🔗

@@ -91,10 +91,13 @@ impl LspAdapter for CssLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "provideFormatter": true
-        }))
+        })))
     }
 }
 

crates/languages/src/deno.rs 🔗

@@ -188,10 +188,13 @@ impl LspAdapter for DenoLspAdapter {
         })
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "provideFormatter": true,
-        }))
+        })))
     }
 
     fn language_ids(&self) -> HashMap<String, String> {

crates/languages/src/go.rs 🔗

@@ -189,8 +189,11 @@ impl super::LspAdapter for GoLspAdapter {
             })
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "usePlaceholders": true,
             "hints": {
                 "assignVariableTypes": true,
@@ -201,7 +204,7 @@ impl super::LspAdapter for GoLspAdapter {
                 "parameterNames": true,
                 "rangeVariableTypes": true
             }
-        }))
+        })))
     }
 
     async fn label_for_completion(

crates/languages/src/html.rs 🔗

@@ -91,10 +91,13 @@ impl LspAdapter for HtmlLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "provideFormatter": true
-        }))
+        })))
     }
 }
 

crates/languages/src/json.rs 🔗

@@ -143,10 +143,13 @@ impl LspAdapter for JsonLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "provideFormatter": true
-        }))
+        })))
     }
 
     fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {

crates/languages/src/php.rs 🔗

@@ -97,24 +97,9 @@ impl LspAdapter for IntelephenseLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    async fn label_for_completion(
-        &self,
-        _item: &lsp::CompletionItem,
-        _language: &Arc<language::Language>,
-    ) -> Option<language::CodeLabel> {
-        None
-    }
-
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        None
-    }
     fn language_ids(&self) -> HashMap<String, String> {
         HashMap::from_iter([("PHP".into(), "php".into())])
     }
-
-    fn prettier_plugins(&self) -> &[&'static str] {
-        &["@prettier/plugin-php"]
-    }
 }
 
 async fn get_cached_server_binary(

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

@@ -15,3 +15,4 @@ collapsed_placeholder = "/* ... */"
 word_characters = ["$"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 prettier_parser_name = "php"
+prettier_plugins = ["@prettier/plugin-php"]

crates/languages/src/prisma.rs 🔗

@@ -88,10 +88,6 @@ impl LspAdapter for PrismaLspAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_server_binary(container_dir, &*self.node).await
     }
-
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        None
-    }
 }
 
 async fn get_cached_server_binary(

crates/languages/src/purescript.rs 🔗

@@ -93,12 +93,15 @@ impl LspAdapter for PurescriptLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "purescript": {
                 "addSpagoSources": true
             }
-        }))
+        })))
     }
 
     fn language_ids(&self) -> HashMap<String, String> {

crates/languages/src/svelte.rs 🔗

@@ -90,7 +90,10 @@ impl LspAdapter for SvelteLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
         let config = json!({
           "inlayHints": {
             "parameterNames": {
@@ -116,17 +119,13 @@ impl LspAdapter for SvelteLspAdapter {
           }
         });
 
-        Some(json!({
+        Ok(Some(json!({
             "provideFormatter": true,
             "configuration": {
               "typescript": config,
               "javascript": config
             }
-        }))
-    }
-
-    fn prettier_plugins(&self) -> &[&'static str] {
-        &["prettier-plugin-svelte"]
+        })))
     }
 }
 

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

@@ -15,6 +15,7 @@ brackets = [
 ]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
 prettier_parser_name = "svelte"
+prettier_plugins = ["prettier-plugin-svelte"]
 
 [overrides.string]
 word_characters = ["-"]

crates/languages/src/tailwind.rs 🔗

@@ -92,8 +92,11 @@ impl LspAdapter for TailwindLspAdapter {
         get_cached_server_binary(container_dir, &*self.node).await
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "provideFormatter": true,
             "userLanguages": {
                 "html": "html",
@@ -101,7 +104,7 @@ impl LspAdapter for TailwindLspAdapter {
                 "javascript": "javascript",
                 "typescriptreact": "typescriptreact",
             },
-        }))
+        })))
     }
 
     fn workspace_configuration(&self, _workspace_root: &Path, _: &mut AppContext) -> Value {
@@ -126,10 +129,6 @@ impl LspAdapter for TailwindLspAdapter {
             ("PHP".to_string(), "php".to_string()),
         ])
     }
-
-    fn prettier_plugins(&self) -> &[&'static str] {
-        &["prettier-plugin-tailwindcss"]
-    }
 }
 
 async fn get_cached_server_binary(

crates/languages/src/typescript.rs 🔗

@@ -164,8 +164,11 @@ impl LspAdapter for TypeScriptLspAdapter {
         })
     }
 
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
             "provideFormatter": true,
             "tsserver": {
                 "path": "node_modules/typescript/lib",
@@ -180,7 +183,7 @@ impl LspAdapter for TypeScriptLspAdapter {
                 "includeInlayFunctionLikeReturnTypeHints": true,
                 "includeInlayEnumMemberValueHints": true,
             }
-        }))
+        })))
     }
 
     fn language_ids(&self) -> HashMap<String, String> {
@@ -367,18 +370,6 @@ impl LspAdapter for EsLintLspAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_eslint_server_binary(container_dir, &*self.node).await
     }
-
-    async fn label_for_completion(
-        &self,
-        _item: &lsp::CompletionItem,
-        _language: &Arc<language::Language>,
-    ) -> Option<language::CodeLabel> {
-        None
-    }
-
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        None
-    }
 }
 
 async fn get_cached_eslint_server_binary(

crates/languages/src/vue.rs 🔗

@@ -5,7 +5,6 @@ pub use language::*;
 use lsp::{CodeActionKind, LanguageServerBinary};
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
-use serde_json::Value;
 use smol::fs::{self};
 use std::{
     any::Any,
@@ -56,17 +55,20 @@ impl super::LspAdapter for VueLspAdapter {
             ts_version: self.node.npm_package_latest_version("typescript").await?,
         }) as Box<_>)
     }
-    fn initialization_options(&self) -> Option<Value> {
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
         let typescript_sdk_path = self.typescript_install_path.lock();
         let typescript_sdk_path = typescript_sdk_path
             .as_ref()
             .expect("initialization_options called without a container_dir for typescript");
 
-        Some(serde_json::json!({
+        Ok(Some(serde_json::json!({
             "typescript": {
                 "tsdk": typescript_sdk_path
             }
-        }))
+        })))
     }
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
         // REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it

crates/node_runtime/src/node_runtime.rs 🔗

@@ -4,8 +4,8 @@ use async_tar::Archive;
 use futures::AsyncReadExt;
 use semver::Version;
 use serde::Deserialize;
-use serde_json::Value;
 use smol::{fs, io::BufReader, lock::Mutex, process::Command};
+use std::io;
 use std::process::{Output, Stdio};
 use std::{
     env::consts,
@@ -46,6 +46,12 @@ pub trait NodeRuntime: Send + Sync {
     async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)])
         -> Result<()>;
 
+    async fn npm_package_installed_version(
+        &self,
+        local_package_directory: &PathBuf,
+        name: &str,
+    ) -> Result<Option<String>>;
+
     async fn should_install_npm_package(
         &self,
         package_name: &str,
@@ -60,36 +66,19 @@ pub trait NodeRuntime: Send + Sync {
             return true;
         }
 
-        let package_json_path = local_package_directory.join("package.json");
-
-        let mut contents = String::new();
-
-        let Some(mut file) = fs::File::open(package_json_path).await.log_err() else {
-            return true;
-        };
-
-        file.read_to_string(&mut contents).await.log_err();
-
-        let Some(package_json): Option<Value> = serde_json::from_str(&contents).log_err() else {
-            return true;
-        };
-
-        let installed_version = package_json
-            .get("dependencies")
-            .and_then(|deps| deps.get(package_name))
-            .and_then(|server_name| server_name.as_str());
-
-        let Some(installed_version) = installed_version else {
+        let Some(installed_version) = self
+            .npm_package_installed_version(local_package_directory, package_name)
+            .await
+            .log_err()
+            .flatten()
+        else {
             return true;
         };
 
-        let Some(latest_version) = Version::parse(latest_version).log_err() else {
+        let Some(installed_version) = Version::parse(&installed_version).log_err() else {
             return true;
         };
-
-        let installed_version = installed_version.trim_start_matches(|c: char| !c.is_ascii_digit());
-
-        let Some(installed_version) = Version::parse(installed_version).log_err() else {
+        let Some(latest_version) = Version::parse(&latest_version).log_err() else {
             return true;
         };
 
@@ -281,6 +270,36 @@ impl NodeRuntime for RealNodeRuntime {
             .ok_or_else(|| anyhow!("no version found for npm package {}", name))
     }
 
+    async fn npm_package_installed_version(
+        &self,
+        local_package_directory: &PathBuf,
+        name: &str,
+    ) -> Result<Option<String>> {
+        let mut package_json_path = local_package_directory.clone();
+        package_json_path.extend(["node_modules", name, "package.json"]);
+
+        let mut file = match fs::File::open(package_json_path).await {
+            Ok(file) => file,
+            Err(err) => {
+                if err.kind() == io::ErrorKind::NotFound {
+                    return Ok(None);
+                }
+
+                Err(err)?
+            }
+        };
+
+        #[derive(Deserialize)]
+        struct PackageJson {
+            version: String,
+        }
+
+        let mut contents = String::new();
+        file.read_to_string(&mut contents).await?;
+        let package_json: PackageJson = serde_json::from_str(&contents)?;
+        Ok(Some(package_json.version))
+    }
+
     async fn npm_install_packages(
         &self,
         directory: &Path,
@@ -335,6 +354,14 @@ impl NodeRuntime for FakeNodeRuntime {
         unreachable!("Should not query npm package '{name}' for latest version")
     }
 
+    async fn npm_package_installed_version(
+        &self,
+        _local_package_directory: &PathBuf,
+        name: &str,
+    ) -> Result<Option<String>> {
+        unreachable!("Should not query npm package '{name}' for installed version")
+    }
+
     async fn npm_install_packages(
         &self,
         _: &Path,

crates/prettier/src/prettier.rs 🔗

@@ -227,13 +227,8 @@ impl Prettier {
                         let buffer_language = buffer.language();
                         let parser_with_plugins = buffer_language.and_then(|l| {
                             let prettier_parser = l.prettier_parser_name()?;
-                            let mut prettier_plugins = local
-                                .language_registry
-                                .lsp_adapters(l)
-                                .iter()
-                                .flat_map(|adapter| adapter.prettier_plugins())
-                                .copied()
-                                .collect::<Vec<_>>();
+                            let mut prettier_plugins =
+                                local.language_registry.all_prettier_plugins();
                             prettier_plugins.dedup();
                             Some((prettier_parser, prettier_plugins))
                         });
@@ -243,8 +238,9 @@ impl Prettier {
                             prettier_node_modules.is_dir(),
                             "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
                         );
-                        let plugin_name_into_path = |plugin_name: &str| {
-                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
+                        let plugin_name_into_path = |plugin_name: Arc<str>| {
+                            let prettier_plugin_dir =
+                                prettier_node_modules.join(plugin_name.as_ref());
                             [
                                 prettier_plugin_dir.join("dist").join("index.mjs"),
                                 prettier_plugin_dir.join("dist").join("index.js"),
@@ -267,8 +263,10 @@ impl Prettier {
 
                                 let mut plugins = plugins
                                     .into_iter()
-                                    .filter(|&plugin_name| {
-                                        if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
+                                    .filter(|plugin_name| {
+                                        if plugin_name.as_ref()
+                                            == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME
+                                        {
                                             add_tailwind_back = true;
                                             false
                                         } else {
@@ -276,14 +274,14 @@ impl Prettier {
                                         }
                                     })
                                     .map(|plugin_name| {
-                                        (plugin_name, plugin_name_into_path(plugin_name))
+                                        (plugin_name.clone(), plugin_name_into_path(plugin_name))
                                     })
                                     .collect::<Vec<_>>();
                                 if add_tailwind_back {
                                     plugins.push((
-                                        &TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
+                                        TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.into(),
                                         plugin_name_into_path(
-                                            TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
+                                            TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.into(),
                                         ),
                                     ));
                                 }

crates/project/src/prettier_support.rs 🔗

@@ -14,7 +14,7 @@ use futures::{
 use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
 use language::{
     language_settings::{Formatter, LanguageSettings},
-    Buffer, Language, LanguageRegistry, LanguageServerName, LocalFile,
+    Buffer, Language, LanguageServerName, LocalFile,
 };
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
@@ -25,28 +25,19 @@ use crate::{
     Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
 };
 
-pub fn prettier_plugins_for_language(
-    language_registry: &Arc<LanguageRegistry>,
-    language: &Arc<Language>,
+pub fn prettier_plugins_for_language<'a>(
+    language: &'a Arc<Language>,
     language_settings: &LanguageSettings,
-) -> Option<HashSet<&'static str>> {
+) -> Option<&'a Vec<Arc<str>>> {
     match &language_settings.formatter {
         Formatter::Prettier { .. } | Formatter::Auto => {}
         Formatter::LanguageServer | Formatter::External { .. } => return None,
     };
-    let mut prettier_plugins = None;
     if language.prettier_parser_name().is_some() {
-        prettier_plugins
-            .get_or_insert_with(|| HashSet::default())
-            .extend(
-                language_registry
-                    .lsp_adapters(language)
-                    .iter()
-                    .flat_map(|adapter| adapter.prettier_plugins()),
-            )
+        Some(language.prettier_plugins())
+    } else {
+        None
     }
-
-    prettier_plugins
 }
 
 pub(super) async fn format_with_prettier(
@@ -114,14 +105,14 @@ pub(super) async fn format_with_prettier(
 
 pub struct DefaultPrettier {
     prettier: PrettierInstallation,
-    installed_plugins: HashSet<&'static str>,
+    installed_plugins: HashSet<Arc<str>>,
 }
 
 pub enum PrettierInstallation {
     NotInstalled {
         attempts: usize,
         installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
-        not_installed_plugins: HashSet<&'static str>,
+        not_installed_plugins: HashSet<Arc<str>>,
     },
     Installed(PrettierInstance),
 }
@@ -376,12 +367,14 @@ fn register_new_prettier(
 
 async fn install_prettier_packages(
     fs: &dyn Fs,
-    plugins_to_install: HashSet<&'static str>,
+    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")).map(
-            |package_name| async {
+    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)
@@ -390,10 +383,10 @@ async fn install_prettier_packages(
                         format!("fetching latest npm version for package {returned_package_name}")
                     })?;
                 anyhow::Ok((returned_package_name, latest_version))
-            },
-        ))
-        .await
-        .context("fetching latest npm versions")?;
+            }),
+    )
+    .await
+    .context("fetching latest npm versions")?;
 
     let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
     match fs.metadata(default_prettier_dir).await.with_context(|| {
@@ -639,32 +632,22 @@ impl Project {
         }
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn install_default_prettier(
-        &mut self,
-        _worktree: Option<WorktreeId>,
-        plugins: HashSet<&'static str>,
-        _cx: &mut ModelContext<Self>,
-    ) {
-        // suppress unused code warnings
-        let _ = should_write_prettier_server_file;
-        let _ = install_prettier_packages;
-        let _ = save_prettier_server_file;
-
-        self.default_prettier.installed_plugins.extend(plugins);
-        self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
-            attempt: 0,
-            prettier: None,
-        });
-    }
-
-    #[cfg(not(any(test, feature = "test-support")))]
     pub fn install_default_prettier(
         &mut self,
         worktree: Option<WorktreeId>,
-        mut new_plugins: HashSet<&'static str>,
+        plugins: impl Iterator<Item = Arc<str>>,
         cx: &mut ModelContext<Self>,
     ) {
+        if cfg!(any(test, feature = "test-support")) {
+            self.default_prettier.installed_plugins.extend(plugins);
+            self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
+                attempt: 0,
+                prettier: None,
+            });
+            return;
+        }
+
+        let mut new_plugins = plugins.collect::<HashSet<_>>();
         let Some(node) = self.node.as_ref().cloned() else {
             return;
         };
@@ -702,7 +685,7 @@ impl Project {
                     );
                     return;
                 }
-                new_plugins.extend(not_installed_plugins.iter());
+                new_plugins.extend(not_installed_plugins.iter().cloned());
                 installation_task.clone()
             }
             PrettierInstallation::Installed { .. } => {
@@ -735,7 +718,7 @@ impl Project {
                                 project.update(&mut cx, |project, _| {
                                     if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
                                         *attempts += 1;
-                                        new_plugins.extend(not_installed_plugins.iter());
+                                        new_plugins.extend(not_installed_plugins.iter().cloned());
                                         installation_attempt = *attempts;
                                         needs_install = true;
                                     };
@@ -761,7 +744,7 @@ impl Project {
                                 not_installed_plugins.retain(|plugin| {
                                     !project.default_prettier.installed_plugins.contains(plugin)
                                 });
-                                not_installed_plugins.extend(new_plugins.iter());
+                                not_installed_plugins.extend(new_plugins.iter().cloned());
                             }
                             needs_install |= !new_plugins.is_empty();
                         })?;

crates/project/src/project.rs 🔗

@@ -994,19 +994,17 @@ impl Project {
 
         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(
-                &self.languages,
-                &language,
-                &settings,
-            ) {
+            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);
+                    .extend(plugins.iter().cloned());
             }
         }
         for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
-            self.install_default_prettier(worktree, prettier_plugins, cx);
+            self.install_default_prettier(worktree, prettier_plugins.into_iter(), cx);
         }
 
         // Start all the newly-enabled language servers.
@@ -2845,12 +2843,10 @@ impl Project {
         let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
         let buffer_file = File::from_dyn(buffer_file.as_ref());
         let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
-        if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language(
-            &self.languages,
-            &new_language,
-            &settings,
-        ) {
-            self.install_default_prettier(worktree, prettier_plugins, cx);
+        if let Some(prettier_plugins) =
+            prettier_support::prettier_plugins_for_language(&new_language, &settings)
+        {
+            self.install_default_prettier(worktree, prettier_plugins.iter().cloned(), cx);
         };
         if let Some(file) = buffer_file {
             let worktree = file.worktree.clone();
@@ -3104,7 +3100,7 @@ impl Project {
     ) -> Result<Arc<LanguageServer>> {
         let workspace_config =
             cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?;
-        let language_server = pending_server.task.await?;
+        let (language_server, mut initialization_options) = pending_server.task.await?;
 
         let name = language_server.name();
         language_server
@@ -3344,7 +3340,6 @@ impl Project {
             })
             .detach();
 
-        let mut initialization_options = adapter.adapter.initialization_options();
         match (&mut initialization_options, override_options) {
             (Some(initialization_options), Some(override_options)) => {
                 merge_json_value_into(override_options, initialization_options);