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