tailwind.rs

  1use anyhow::{anyhow, Result};
  2use async_trait::async_trait;
  3use collections::HashMap;
  4use futures::StreamExt;
  5use gpui::AsyncAppContext;
  6use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
  7use lsp::LanguageServerBinary;
  8use node_runtime::NodeRuntime;
  9use project::lsp_store::language_server_settings;
 10use serde_json::{json, Value};
 11use smol::fs;
 12use std::{
 13    any::Any,
 14    ffi::OsString,
 15    path::{Path, PathBuf},
 16    sync::Arc,
 17};
 18use util::{maybe, ResultExt};
 19
 20#[cfg(target_os = "windows")]
 21const SERVER_PATH: &str =
 22    "node_modules/@tailwindcss/language-server/bin/tailwindcss-language-server";
 23#[cfg(not(target_os = "windows"))]
 24const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
 25
 26fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 27    vec![server_path.into(), "--stdio".into()]
 28}
 29
 30pub struct TailwindLspAdapter {
 31    node: Arc<dyn NodeRuntime>,
 32}
 33
 34impl TailwindLspAdapter {
 35    const SERVER_NAME: &'static str = "tailwindcss-language-server";
 36
 37    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
 38        TailwindLspAdapter { node }
 39    }
 40}
 41
 42#[async_trait(?Send)]
 43impl LspAdapter for TailwindLspAdapter {
 44    fn name(&self) -> LanguageServerName {
 45        LanguageServerName(Self::SERVER_NAME.into())
 46    }
 47
 48    async fn check_if_user_installed(
 49        &self,
 50        delegate: &dyn LspAdapterDelegate,
 51        cx: &AsyncAppContext,
 52    ) -> Option<LanguageServerBinary> {
 53        let configured_binary = cx
 54            .update(|cx| {
 55                language_server_settings(delegate, Self::SERVER_NAME, cx)
 56                    .and_then(|s| s.binary.clone())
 57            })
 58            .ok()??;
 59
 60        let path = if let Some(configured_path) = configured_binary.path.map(PathBuf::from) {
 61            configured_path
 62        } else {
 63            self.node.binary_path().await.ok()?
 64        };
 65
 66        let arguments = configured_binary
 67            .arguments
 68            .unwrap_or_default()
 69            .iter()
 70            .map(|arg| arg.into())
 71            .collect();
 72
 73        Some(LanguageServerBinary {
 74            path,
 75            arguments,
 76            env: None,
 77        })
 78    }
 79
 80    async fn fetch_latest_server_version(
 81        &self,
 82        _: &dyn LspAdapterDelegate,
 83    ) -> Result<Box<dyn 'static + Any + Send>> {
 84        Ok(Box::new(
 85            self.node
 86                .npm_package_latest_version("@tailwindcss/language-server")
 87                .await?,
 88        ) as Box<_>)
 89    }
 90
 91    async fn fetch_server_binary(
 92        &self,
 93        latest_version: Box<dyn 'static + Send + Any>,
 94        container_dir: PathBuf,
 95        _: &dyn LspAdapterDelegate,
 96    ) -> Result<LanguageServerBinary> {
 97        let latest_version = latest_version.downcast::<String>().unwrap();
 98        let server_path = container_dir.join(SERVER_PATH);
 99        let package_name = "@tailwindcss/language-server";
100
101        let should_install_language_server = self
102            .node
103            .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
104            .await;
105
106        if should_install_language_server {
107            self.node
108                .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
109                .await?;
110        }
111
112        Ok(LanguageServerBinary {
113            path: self.node.binary_path().await?,
114            env: None,
115            arguments: server_binary_arguments(&server_path),
116        })
117    }
118
119    async fn cached_server_binary(
120        &self,
121        container_dir: PathBuf,
122        _: &dyn LspAdapterDelegate,
123    ) -> Option<LanguageServerBinary> {
124        get_cached_server_binary(container_dir, &*self.node).await
125    }
126
127    async fn installation_test_binary(
128        &self,
129        container_dir: PathBuf,
130    ) -> Option<LanguageServerBinary> {
131        get_cached_server_binary(container_dir, &*self.node).await
132    }
133
134    async fn initialization_options(
135        self: Arc<Self>,
136        _: &Arc<dyn LspAdapterDelegate>,
137    ) -> Result<Option<serde_json::Value>> {
138        Ok(Some(json!({
139            "provideFormatter": true,
140            "userLanguages": {
141                "html": "html",
142                "css": "css",
143                "javascript": "javascript",
144                "typescriptreact": "typescriptreact",
145            },
146        })))
147    }
148
149    async fn workspace_configuration(
150        self: Arc<Self>,
151        delegate: &Arc<dyn LspAdapterDelegate>,
152        cx: &mut AsyncAppContext,
153    ) -> Result<Value> {
154        let tailwind_user_settings = cx.update(|cx| {
155            language_server_settings(delegate.as_ref(), Self::SERVER_NAME, cx)
156                .and_then(|s| s.settings.clone())
157                .unwrap_or_default()
158        })?;
159
160        let mut configuration = json!({
161            "tailwindCSS": {
162                "emmetCompletions": true,
163            }
164        });
165
166        if let Some(experimental) = tailwind_user_settings.get("experimental").cloned() {
167            configuration["tailwindCSS"]["experimental"] = experimental;
168        }
169
170        if let Some(class_attributes) = tailwind_user_settings.get("classAttributes").cloned() {
171            configuration["tailwindCSS"]["classAttributes"] = class_attributes;
172        }
173
174        if let Some(include_languages) = tailwind_user_settings.get("includeLanguages").cloned() {
175            configuration["tailwindCSS"]["includeLanguages"] = include_languages;
176        }
177
178        Ok(configuration)
179    }
180
181    fn language_ids(&self) -> HashMap<String, String> {
182        HashMap::from_iter([
183            ("Astro".to_string(), "astro".to_string()),
184            ("HTML".to_string(), "html".to_string()),
185            ("CSS".to_string(), "css".to_string()),
186            ("JavaScript".to_string(), "javascript".to_string()),
187            ("TSX".to_string(), "typescriptreact".to_string()),
188            ("Svelte".to_string(), "svelte".to_string()),
189            ("Elixir".to_string(), "phoenix-heex".to_string()),
190            ("HEEX".to_string(), "phoenix-heex".to_string()),
191            ("ERB".to_string(), "erb".to_string()),
192            ("PHP".to_string(), "php".to_string()),
193            ("Vue.js".to_string(), "vue".to_string()),
194        ])
195    }
196}
197
198async fn get_cached_server_binary(
199    container_dir: PathBuf,
200    node: &dyn NodeRuntime,
201) -> Option<LanguageServerBinary> {
202    maybe!(async {
203        let mut last_version_dir = None;
204        let mut entries = fs::read_dir(&container_dir).await?;
205        while let Some(entry) = entries.next().await {
206            let entry = entry?;
207            if entry.file_type().await?.is_dir() {
208                last_version_dir = Some(entry.path());
209            }
210        }
211        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
212        let server_path = last_version_dir.join(SERVER_PATH);
213        if server_path.exists() {
214            Ok(LanguageServerBinary {
215                path: node.binary_path().await?,
216                env: None,
217                arguments: server_binary_arguments(&server_path),
218            })
219        } else {
220            Err(anyhow!(
221                "missing executable in directory {:?}",
222                last_version_dir
223            ))
224        }
225    })
226    .await
227    .log_err()
228}