language: Add support for Tailwind CSS Mode (#45352)

Closes #44904

Adds the Tailwind CSS LSP as suggested here:
https://github.com/zed-industries/zed/pull/39517#issuecomment-3368206678.
This allows users to use the Tailwind CSS LSP for CSS files, to support
Tailwind specific rules.

Docs will be added in a separate PR after this is released

Release Notes:

- Add Tailwind CSS Mode support, for Tailwind specific CSS rules like
`@apply`, `@layer`, and `@theme`.
This commit is contained in:
Gaauwe Rombouts authored and GitHub committed 2026-01-05 11:40:54 +01:00
1 parent 5694eb8f43
commit 3d9f35d704
3 files changed
+202 -2

No files matched your search

+2 -2
View File
@@ -9,6 +9,6 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]
completion_query_characters = ["-"]
completion_query_characters = ["-", "@"]
block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
prettier_parser_name = "css"
prettier_parser_name = "css"
+6
View File
@@ -28,6 +28,7 @@ mod package_json;
mod python;
mod rust;
mod tailwind;
mod tailwindcss;
mod typescript;
mod vtsls;
mod yaml;
@@ -101,6 +102,7 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
let rust_context_provider = Arc::new(rust::RustContextProvider);
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone()));
let tailwindcss_adapter = Arc::new(tailwindcss::TailwindCssLspAdapter::new(node.clone()));
let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new(fs.clone()));
let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(
node.clone(),
@@ -261,6 +263,10 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
LanguageServerName("tailwindcss-language-server".into()),
tailwind_adapter.clone(),
);
languages.register_available_lsp_adapter(
LanguageServerName("tailwindcss-intellisense-css".into()),
tailwindcss_adapter,
);
languages.register_available_lsp_adapter(
LanguageServerName("eslint".into()),
eslint_adapter.clone(),
+194
View File
@@ -0,0 +1,194 @@
use anyhow::Result;
use async_trait::async_trait;
use gpui::AsyncApp;
use language::{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::json;
use std::{
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, maybe, merge_json_value_into};
const SERVER_PATH: &str = "node_modules/@tailwindcss/language-server/bin/css-language-server";
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
pub struct TailwindCssLspAdapter {
node: NodeRuntime,
}
// Implements the LSP adapter for the Tailwind CSS LSP fork: https://github.com/zed-industries/zed/pull/39517#issuecomment-3368206678
impl TailwindCssLspAdapter {
const SERVER_NAME: LanguageServerName =
LanguageServerName::new_static("tailwindcss-intellisense-css");
const PACKAGE_NAME: &str = "@tailwindcss/language-server";
pub fn new(node: NodeRuntime) -> Self {
TailwindCssLspAdapter { node }
}
}
impl LspInstaller for TailwindCssLspAdapter {
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 TailwindCssLspAdapter {
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<serde_json::Value> {
let mut default_config = json!({
"css": {
"lint": {}
},
"less": {
"lint": {}
},
"scss": {
"lint": {}
}
});
let project_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &self.name(), cx)
.and_then(|s| s.settings.clone())
})?;
if let Some(override_options) = project_options {
merge_json_value_into(override_options, &mut default_config);
}
Ok(default_config)
}
}
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()
}