use anyhow::Result;use async_trait::async_trait;use collections::HashMap;use gpui::AsyncApp;use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};use lsp::{LanguageServerBinary, LanguageServerName, Uri};use node_runtime::{NodeRuntime, VersionStrategy};use project::lsp_store::language_server_settings;use semver::Version;use serde_json::{Value, json};use std::{ ffi::OsString, path::{Path, PathBuf}, sync::Arc,};use util::{ResultExt, maybe}; #[cfg(target_os = "windows")]const SERVER_PATH: &str = "node_modules/@tailwindcss/language-server/bin/tailwindcss-language-server";#[cfg(not(target_os = "windows"))]const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server"; fn server_binary_arguments(server_path: &Path) -> Vec<OsString> { vec![server_path.into(), "--stdio".into()]} pub struct TailwindLspAdapter { node: NodeRuntime,} impl TailwindLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("tailwindcss-language-server"); const PACKAGE_NAME: &str = "@tailwindcss/language-server"; pub fn new(node: NodeRuntime) -> Self { TailwindLspAdapter { node } }} impl LspInstaller for TailwindLspAdapter { type BinaryVersion = Version; async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, _: bool, _: &mut AsyncApp, ) -> Result<Self::BinaryVersion> { self.node .npm_package_latest_version(Self::PACKAGE_NAME) .await } async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, _: Option<Toolchain>, _: &AsyncApp, ) -> Option<LanguageServerBinary> { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; let env = delegate.shell_env().await; Some(LanguageServerBinary { path, env: Some(env), arguments: vec!["--stdio".into()], }) } async fn fetch_server_binary( &self, latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result<LanguageServerBinary> { let server_path = container_dir.join(SERVER_PATH); let latest_version = latest_version.to_string(); self.node .npm_install_packages( &container_dir, &[(Self::PACKAGE_NAME, latest_version.as_str())], ) .await?; Ok(LanguageServerBinary { path: self.node.binary_path().await?, env: None, arguments: server_binary_arguments(&server_path), }) } async fn check_if_version_installed( &self, version: &Self::BinaryVersion, container_dir: &PathBuf, _: &dyn LspAdapterDelegate, ) -> Option<LanguageServerBinary> { let server_path = container_dir.join(SERVER_PATH); let should_install_language_server = self .node .should_install_npm_package( Self::PACKAGE_NAME, &server_path, container_dir, VersionStrategy::Latest(version), ) .await; if should_install_language_server { None } else { Some(LanguageServerBinary { path: self.node.binary_path().await.ok()?, env: None, arguments: server_binary_arguments(&server_path), }) } } async fn cached_server_binary( &self, container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option<LanguageServerBinary> { get_cached_server_binary(container_dir, &self.node).await }} #[async_trait(?Send)]impl LspAdapter for TailwindLspAdapter { fn name(&self) -> LanguageServerName { Self::SERVER_NAME } async fn initialization_options( self: Arc<Self>, _: &Arc<dyn LspAdapterDelegate>, ) -> Result<Option<serde_json::Value>> { Ok(Some(json!({ "provideFormatter": true, }))) } async fn workspace_configuration( self: Arc<Self>, delegate: &Arc<dyn LspAdapterDelegate>, _: Option<Toolchain>, _: Option<Uri>, cx: &mut AsyncApp, ) -> Result<Value> { let mut tailwind_user_settings = cx.update(|cx| { language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx) .and_then(|s| s.settings.clone()) .unwrap_or_default() }); if tailwind_user_settings.get("emmetCompletions").is_none() { tailwind_user_settings["emmetCompletions"] = Value::Bool(true); } if tailwind_user_settings.get("includeLanguages").is_none() { tailwind_user_settings["includeLanguages"] = json!({ "html": "html", "css": "css", "javascript": "javascript", "typescript": "typescript", "typescriptreact": "typescriptreact", }); } Ok(json!({ "tailwindCSS": tailwind_user_settings })) } fn language_ids(&self) -> HashMap<LanguageName, String> { HashMap::from_iter([ (LanguageName::new_static("Astro"), "astro".to_string()), (LanguageName::new_static("HTML"), "html".to_string()), (LanguageName::new_static("Gleam"), "html".to_string()), (LanguageName::new_static("CSS"), "css".to_string()), ( LanguageName::new_static("JavaScript"), "javascript".to_string(), ), ( LanguageName::new_static("TypeScript"), "typescript".to_string(), ), ( LanguageName::new_static("TSX"), "typescriptreact".to_string(), ), (LanguageName::new_static("Svelte"), "svelte".to_string()), ( LanguageName::new_static("Elixir"), "phoenix-heex".to_string(), ), (LanguageName::new_static("HEEX"), "phoenix-heex".to_string()), (LanguageName::new_static("ERB"), "erb".to_string()), (LanguageName::new_static("HTML+ERB"), "erb".to_string()), (LanguageName::new_static("PHP"), "php".to_string()), (LanguageName::new_static("Vue.js"), "vue".to_string()), ]) }} async fn get_cached_server_binary( container_dir: PathBuf, node: &NodeRuntime,) -> Option<LanguageServerBinary> { maybe!(async { let server_path = container_dir.join(SERVER_PATH); anyhow::ensure!( server_path.exists(), "missing executable in directory {server_path:?}" ); Ok(LanguageServerBinary { path: node.binary_path().await?, env: None, arguments: server_binary_arguments(&server_path), }) }) .await .log_err()}
use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncApp;
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use semver::Version;
use serde_json::{Value, json};
use std::{
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, maybe};
#[cfg(target_os = "windows")]
const SERVER_PATH: &str =
"node_modules/@tailwindcss/language-server/bin/tailwindcss-language-server";
#[cfg(not(target_os = "windows"))]
const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
pub struct TailwindLspAdapter {
node: NodeRuntime,
}
impl TailwindLspAdapter {
const SERVER_NAME: LanguageServerName =
LanguageServerName::new_static("tailwindcss-language-server");
const PACKAGE_NAME: &str = "@tailwindcss/language-server";
pub fn new(node: NodeRuntime) -> Self {
TailwindLspAdapter { node }
}
}
impl LspInstaller for TailwindLspAdapter {
type BinaryVersion = Version;
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
_: bool,
_: &mut AsyncApp,
) -> Result<Self::BinaryVersion> {
self.node
.npm_package_latest_version(Self::PACKAGE_NAME)
.await
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
let env = delegate.shell_env().await;
Some(LanguageServerBinary {
path,
env: Some(env),
arguments: vec!["--stdio".into()],
})
}
async fn fetch_server_binary(
&self,
latest_version: Self::BinaryVersion,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let server_path = container_dir.join(SERVER_PATH);
let latest_version = latest_version.to_string();
self.node
.npm_install_packages(
&container_dir,
&[(Self::PACKAGE_NAME, latest_version.as_str())],
)
.await?;
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
async fn check_if_version_installed(
&self,
version: &Self::BinaryVersion,
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
container_dir,
VersionStrategy::Latest(version),
)
.await;
if should_install_language_server {
None
} else {
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &self.node).await
}
}
#[async_trait(?Send)]
impl LspAdapter for TailwindLspAdapter {
fn name(&self) -> LanguageServerName {
Self::SERVER_NAME
}
async fn initialization_options(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
"provideFormatter": true,
})))
}
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
_: Option<Uri>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut tailwind_user_settings = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
.unwrap_or_default()
});
if tailwind_user_settings.get("emmetCompletions").is_none() {
tailwind_user_settings["emmetCompletions"] = Value::Bool(true);
}
if tailwind_user_settings.get("includeLanguages").is_none() {
tailwind_user_settings["includeLanguages"] = json!({
"html": "html",
"css": "css",
"javascript": "javascript",
"typescript": "typescript",
"typescriptreact": "typescriptreact",
});
}
Ok(json!({
"tailwindCSS": tailwind_user_settings
}))
}
fn language_ids(&self) -> HashMap<LanguageName, String> {
HashMap::from_iter([
(LanguageName::new_static("Astro"), "astro".to_string()),
(LanguageName::new_static("HTML"), "html".to_string()),
(LanguageName::new_static("Gleam"), "html".to_string()),
(LanguageName::new_static("CSS"), "css".to_string()),
(
LanguageName::new_static("JavaScript"),
"javascript".to_string(),
),
(
LanguageName::new_static("TypeScript"),
"typescript".to_string(),
),
(
LanguageName::new_static("TSX"),
"typescriptreact".to_string(),
),
(LanguageName::new_static("Svelte"), "svelte".to_string()),
(
LanguageName::new_static("Elixir"),
"phoenix-heex".to_string(),
),
(LanguageName::new_static("HEEX"), "phoenix-heex".to_string()),
(LanguageName::new_static("ERB"), "erb".to_string()),
(LanguageName::new_static("HTML+ERB"), "erb".to_string()),
(LanguageName::new_static("PHP"), "php".to_string()),
(LanguageName::new_static("Vue.js"), "vue".to_string()),
])
}
}
async fn get_cached_server_binary(
container_dir: PathBuf,
node: &NodeRuntime,
) -> Option<LanguageServerBinary> {
maybe!(async {
let server_path = container_dir.join(SERVER_PATH);
anyhow::ensure!(
server_path.exists(),
"missing executable in directory {server_path:?}"
);
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
})
.await
.log_err()
}