Files
zed/crates/languages/src/tailwind.rs
Smit Barmase a01a2ed0e0 languages: Add Tailwind CSS support for TypeScript (#38254)
Closes #37028

I noticed many projects use Tailwind in plain TypeScript (.ts) files, so
it makes sense to support them out of the box, alongside .js and .tsx
files we already handle. For example, see
[supabase](https://github.com/supabase/supabase/blob/master/packages/ui/src/lib/theme/defaultTheme.ts).

Note: You’ll still need to add `"classFunctions": ["cva", "cx"],`
manually for Tailwind completions to work in `cva` type methods. This is
because you don’t want completions on every string, only in specific
methods or regex matches. This is documented.

Release Notes:

- Added out-of-the-box support for Tailwind completions in `.ts` files.
2025-09-16 20:06:14 +05:30

224 lines
6.8 KiB
Rust

use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use gpui::AsyncApp;
use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
use smol::fs;
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 = String;
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
_: bool,
_: &mut AsyncApp,
) -> Result<String> {
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: String,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let server_path = container_dir.join(SERVER_PATH);
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: &String,
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,
"userLanguages": {
"html": "html",
"css": "css",
"javascript": "javascript",
"typescript": "typescript",
"typescriptreact": "typescriptreact",
},
})))
}
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Option<Toolchain>,
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);
}
Ok(json!({
"tailwindCSS": tailwind_user_settings,
}))
}
fn language_ids(&self) -> HashMap<LanguageName, String> {
HashMap::from_iter([
(LanguageName::new("Astro"), "astro".to_string()),
(LanguageName::new("HTML"), "html".to_string()),
(LanguageName::new("CSS"), "css".to_string()),
(LanguageName::new("JavaScript"), "javascript".to_string()),
(LanguageName::new("TypeScript"), "typescript".to_string()),
(LanguageName::new("TSX"), "typescriptreact".to_string()),
(LanguageName::new("Svelte"), "svelte".to_string()),
(LanguageName::new("Elixir"), "phoenix-heex".to_string()),
(LanguageName::new("HEEX"), "phoenix-heex".to_string()),
(LanguageName::new("ERB"), "erb".to_string()),
(LanguageName::new("HTML+ERB"), "erb".to_string()),
(LanguageName::new("HTML/ERB"), "erb".to_string()),
(LanguageName::new("PHP"), "php".to_string()),
(LanguageName::new("Vue.js"), "vue".to_string()),
])
}
}
async fn get_cached_server_binary(
container_dir: PathBuf,
node: &NodeRuntime,
) -> Option<LanguageServerBinary> {
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_dir() {
last_version_dir = Some(entry.path());
}
}
let last_version_dir = last_version_dir.context("no cached binary")?;
let server_path = last_version_dir.join(SERVER_PATH);
anyhow::ensure!(
server_path.exists(),
"missing executable in directory {last_version_dir:?}"
);
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
})
.await
.log_err()
}