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

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>
This commit is contained in:
Max Brunsfeld 2024-03-20 12:47:04 -07:00 committed by GitHub
parent 0ce5cdc48f
commit d699b8e104
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 318 additions and 208 deletions

View file

@ -92,4 +92,37 @@ impl LspAdapter for ExtensionLspAdapter {
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> { async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
None 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
})
}
} }

View file

@ -307,6 +307,46 @@ impl wit::ExtensionImports for WasmState {
.map_err(|err| err.to_string())) .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( async fn latest_github_release(
&mut self, &mut self,
repo: String, repo: String,

View file

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

View file

@ -51,6 +51,12 @@ world extension {
/// Gets the latest version of the given NPM package. /// Gets the latest version of the given NPM package.
import npm-package-latest-version: func(package-name: string) -> result<string, string>; 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. /// Gets the latest release for the given GitHub repository.
import latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>; 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-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>;
} }

View file

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

View file

@ -63,7 +63,7 @@ pub enum LanguageServerBinaryStatus {
pub struct PendingLanguageServer { pub struct PendingLanguageServer {
pub server_id: LanguageServerId, 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>>, pub container_dir: Option<Arc<Path>>,
} }
@ -629,6 +629,15 @@ impl LanguageRegistry {
.unwrap_or_default() .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( pub fn update_lsp_status(
&self, &self,
server_name: LanguageServerName, server_name: LanguageServerName,
@ -680,6 +689,12 @@ impl LanguageRegistry {
) )
.await?; .await?;
let options = adapter
.adapter
.clone()
.initialization_options(&delegate)
.await?;
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) { if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
task.await?; task.await?;
} }
@ -727,10 +742,11 @@ impl LanguageRegistry {
}) })
.detach(); .detach();
return Ok(server); return Ok((server, options));
} }
drop(this); drop(this);
Ok((
lsp::LanguageServer::new( lsp::LanguageServer::new(
stderr_capture, stderr_capture,
server_id, server_id,
@ -738,7 +754,9 @@ impl LanguageRegistry {
&root_path, &root_path,
adapter.code_action_kinds(), adapter.code_action_kinds(),
cx, cx,
) )?,
options,
))
} }
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -97,24 +97,9 @@ impl LspAdapter for IntelephenseLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await 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> { fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([("PHP".into(), "php".into())]) HashMap::from_iter([("PHP".into(), "php".into())])
} }
fn prettier_plugins(&self) -> &[&'static str] {
&["@prettier/plugin-php"]
}
} }
async fn get_cached_server_binary( async fn get_cached_server_binary(

View file

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

View file

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

View file

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

View file

@ -90,7 +90,10 @@ impl LspAdapter for SvelteLspAdapter {
get_cached_server_binary(container_dir, &*self.node).await 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!({ let config = json!({
"inlayHints": { "inlayHints": {
"parameterNames": { "parameterNames": {
@ -116,17 +119,13 @@ impl LspAdapter for SvelteLspAdapter {
} }
}); });
Some(json!({ Ok(Some(json!({
"provideFormatter": true, "provideFormatter": true,
"configuration": { "configuration": {
"typescript": config, "typescript": config,
"javascript": config "javascript": config
} }
})) })))
}
fn prettier_plugins(&self) -> &[&'static str] {
&["prettier-plugin-svelte"]
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@ use async_tar::Archive;
use futures::AsyncReadExt; use futures::AsyncReadExt;
use semver::Version; use semver::Version;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value;
use smol::{fs, io::BufReader, lock::Mutex, process::Command}; use smol::{fs, io::BufReader, lock::Mutex, process::Command};
use std::io;
use std::process::{Output, Stdio}; use std::process::{Output, Stdio};
use std::{ use std::{
env::consts, env::consts,
@ -46,6 +46,12 @@ pub trait NodeRuntime: Send + Sync {
async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)]) async fn npm_install_packages(&self, directory: &Path, packages: &[(&str, &str)])
-> Result<()>; -> Result<()>;
async fn npm_package_installed_version(
&self,
local_package_directory: &PathBuf,
name: &str,
) -> Result<Option<String>>;
async fn should_install_npm_package( async fn should_install_npm_package(
&self, &self,
package_name: &str, package_name: &str,
@ -60,36 +66,19 @@ pub trait NodeRuntime: Send + Sync {
return true; return true;
} }
let package_json_path = local_package_directory.join("package.json"); let Some(installed_version) = self
.npm_package_installed_version(local_package_directory, package_name)
let mut contents = String::new(); .await
.log_err()
let Some(mut file) = fs::File::open(package_json_path).await.log_err() else { .flatten()
else {
return true; return true;
}; };
file.read_to_string(&mut contents).await.log_err(); let Some(installed_version) = Version::parse(&installed_version).log_err() else {
let Some(package_json): Option<Value> = serde_json::from_str(&contents).log_err() else {
return true; return true;
}; };
let Some(latest_version) = Version::parse(&latest_version).log_err() else {
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 {
return true;
};
let Some(latest_version) = Version::parse(latest_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 {
return true; return true;
}; };
@ -281,6 +270,36 @@ impl NodeRuntime for RealNodeRuntime {
.ok_or_else(|| anyhow!("no version found for npm package {}", name)) .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( async fn npm_install_packages(
&self, &self,
directory: &Path, directory: &Path,
@ -335,6 +354,14 @@ impl NodeRuntime for FakeNodeRuntime {
unreachable!("Should not query npm package '{name}' for latest version") 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( async fn npm_install_packages(
&self, &self,
_: &Path, _: &Path,

View file

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

View file

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

View file

@ -994,19 +994,17 @@ impl Project {
let mut prettier_plugins_by_worktree = HashMap::default(); let mut prettier_plugins_by_worktree = HashMap::default();
for (worktree, language, settings) in language_formatters_to_check { for (worktree, language, settings) in language_formatters_to_check {
if let Some(plugins) = prettier_support::prettier_plugins_for_language( if let Some(plugins) =
&self.languages, prettier_support::prettier_plugins_for_language(&language, &settings)
&language, {
&settings,
) {
prettier_plugins_by_worktree prettier_plugins_by_worktree
.entry(worktree) .entry(worktree)
.or_insert_with(|| HashSet::default()) .or_insert_with(|| HashSet::default())
.extend(plugins); .extend(plugins.iter().cloned());
} }
} }
for (worktree, prettier_plugins) in prettier_plugins_by_worktree { 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. // 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 settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
let buffer_file = File::from_dyn(buffer_file.as_ref()); let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx)); let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
if let Some(prettier_plugins) = prettier_support::prettier_plugins_for_language( if let Some(prettier_plugins) =
&self.languages, prettier_support::prettier_plugins_for_language(&new_language, &settings)
&new_language, {
&settings, self.install_default_prettier(worktree, prettier_plugins.iter().cloned(), cx);
) {
self.install_default_prettier(worktree, prettier_plugins, cx);
}; };
if let Some(file) = buffer_file { if let Some(file) = buffer_file {
let worktree = file.worktree.clone(); let worktree = file.worktree.clone();
@ -3104,7 +3100,7 @@ impl Project {
) -> Result<Arc<LanguageServer>> { ) -> Result<Arc<LanguageServer>> {
let workspace_config = let workspace_config =
cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?; 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(); let name = language_server.name();
language_server language_server
@ -3344,7 +3340,6 @@ impl Project {
}) })
.detach(); .detach();
let mut initialization_options = adapter.adapter.initialization_options();
match (&mut initialization_options, override_options) { match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => { (Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options); merge_json_value_into(override_options, initialization_options);