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