tailwindcss.rs

  1use anyhow::Result;
  2use async_trait::async_trait;
  3use gpui::AsyncApp;
  4use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
  5use lsp::{LanguageServerBinary, LanguageServerName, Uri};
  6use node_runtime::{NodeRuntime, VersionStrategy};
  7use project::lsp_store::language_server_settings;
  8use semver::Version;
  9use serde_json::json;
 10use std::{
 11    ffi::OsString,
 12    path::{Path, PathBuf},
 13    sync::Arc,
 14};
 15use util::{ResultExt, maybe, merge_json_value_into};
 16
 17const SERVER_PATH: &str = "node_modules/@tailwindcss/language-server/bin/css-language-server";
 18
 19fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 20    vec![server_path.into(), "--stdio".into()]
 21}
 22
 23pub struct TailwindCssLspAdapter {
 24    node: NodeRuntime,
 25}
 26
 27// Implements the LSP adapter for the Tailwind CSS LSP fork: https://github.com/zed-industries/zed/pull/39517#issuecomment-3368206678
 28impl TailwindCssLspAdapter {
 29    const SERVER_NAME: LanguageServerName =
 30        LanguageServerName::new_static("tailwindcss-intellisense-css");
 31    const PACKAGE_NAME: &str = "@tailwindcss/language-server";
 32
 33    pub fn new(node: NodeRuntime) -> Self {
 34        TailwindCssLspAdapter { node }
 35    }
 36}
 37
 38impl LspInstaller for TailwindCssLspAdapter {
 39    type BinaryVersion = Version;
 40
 41    async fn fetch_latest_server_version(
 42        &self,
 43        _: &dyn LspAdapterDelegate,
 44        _: bool,
 45        _: &mut AsyncApp,
 46    ) -> Result<Self::BinaryVersion> {
 47        self.node
 48            .npm_package_latest_version(Self::PACKAGE_NAME)
 49            .await
 50    }
 51
 52    async fn check_if_user_installed(
 53        &self,
 54        delegate: &dyn LspAdapterDelegate,
 55        _: Option<Toolchain>,
 56        _: &AsyncApp,
 57    ) -> Option<LanguageServerBinary> {
 58        let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
 59        let env = delegate.shell_env().await;
 60
 61        Some(LanguageServerBinary {
 62            path,
 63            env: Some(env),
 64            arguments: vec!["--stdio".into()],
 65        })
 66    }
 67
 68    async fn fetch_server_binary(
 69        &self,
 70        latest_version: Self::BinaryVersion,
 71        container_dir: PathBuf,
 72        _: &dyn LspAdapterDelegate,
 73    ) -> Result<LanguageServerBinary> {
 74        let server_path = container_dir.join(SERVER_PATH);
 75        let latest_version = latest_version.to_string();
 76
 77        self.node
 78            .npm_install_packages(
 79                &container_dir,
 80                &[(Self::PACKAGE_NAME, latest_version.as_str())],
 81            )
 82            .await?;
 83
 84        Ok(LanguageServerBinary {
 85            path: self.node.binary_path().await?,
 86            env: None,
 87            arguments: server_binary_arguments(&server_path),
 88        })
 89    }
 90
 91    async fn check_if_version_installed(
 92        &self,
 93        version: &Self::BinaryVersion,
 94        container_dir: &PathBuf,
 95        _: &dyn LspAdapterDelegate,
 96    ) -> Option<LanguageServerBinary> {
 97        let server_path = container_dir.join(SERVER_PATH);
 98
 99        let should_install_language_server = self
100            .node
101            .should_install_npm_package(
102                Self::PACKAGE_NAME,
103                &server_path,
104                container_dir,
105                VersionStrategy::Latest(version),
106            )
107            .await;
108
109        if should_install_language_server {
110            None
111        } else {
112            Some(LanguageServerBinary {
113                path: self.node.binary_path().await.ok()?,
114                env: None,
115                arguments: server_binary_arguments(&server_path),
116            })
117        }
118    }
119
120    async fn cached_server_binary(
121        &self,
122        container_dir: PathBuf,
123        _: &dyn LspAdapterDelegate,
124    ) -> Option<LanguageServerBinary> {
125        get_cached_server_binary(container_dir, &self.node).await
126    }
127}
128
129#[async_trait(?Send)]
130impl LspAdapter for TailwindCssLspAdapter {
131    fn name(&self) -> LanguageServerName {
132        Self::SERVER_NAME
133    }
134
135    async fn initialization_options(
136        self: Arc<Self>,
137        _: &Arc<dyn LspAdapterDelegate>,
138    ) -> Result<Option<serde_json::Value>> {
139        Ok(Some(json!({
140            "provideFormatter": true
141        })))
142    }
143
144    async fn workspace_configuration(
145        self: Arc<Self>,
146        delegate: &Arc<dyn LspAdapterDelegate>,
147        _: Option<Toolchain>,
148        _: Option<Uri>,
149        cx: &mut AsyncApp,
150    ) -> Result<serde_json::Value> {
151        let mut default_config = json!({
152            "css": {
153                "lint": {}
154            },
155            "less": {
156                "lint": {}
157            },
158            "scss": {
159                "lint": {}
160            }
161        });
162
163        let project_options = cx.update(|cx| {
164            language_server_settings(delegate.as_ref(), &self.name(), cx)
165                .and_then(|s| s.settings.clone())
166        })?;
167
168        if let Some(override_options) = project_options {
169            merge_json_value_into(override_options, &mut default_config);
170        }
171
172        Ok(default_config)
173    }
174}
175
176async fn get_cached_server_binary(
177    container_dir: PathBuf,
178    node: &NodeRuntime,
179) -> Option<LanguageServerBinary> {
180    maybe!(async {
181        let server_path = container_dir.join(SERVER_PATH);
182        anyhow::ensure!(
183            server_path.exists(),
184            "missing executable in directory {server_path:?}"
185        );
186        Ok(LanguageServerBinary {
187            path: node.binary_path().await?,
188            env: None,
189            arguments: server_binary_arguments(&server_path),
190        })
191    })
192    .await
193    .log_err()
194}