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