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