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 fetch_latest_server_version(
 46        &self,
 47        _: &dyn LspAdapterDelegate,
 48    ) -> Result<Box<dyn 'static + Any + Send>> {
 49        Ok(Box::new(
 50            self.node
 51                .npm_package_latest_version("@tailwindcss/language-server")
 52                .await?,
 53        ) as Box<_>)
 54    }
 55
 56    async fn fetch_server_binary(
 57        &self,
 58        latest_version: Box<dyn 'static + Send + Any>,
 59        container_dir: PathBuf,
 60        _: &dyn LspAdapterDelegate,
 61    ) -> Result<LanguageServerBinary> {
 62        let latest_version = latest_version.downcast::<String>().unwrap();
 63        let server_path = container_dir.join(SERVER_PATH);
 64        let package_name = "@tailwindcss/language-server";
 65
 66        let should_install_language_server = self
 67            .node
 68            .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
 69            .await;
 70
 71        if should_install_language_server {
 72            self.node
 73                .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
 74                .await?;
 75        }
 76
 77        Ok(LanguageServerBinary {
 78            path: self.node.binary_path().await?,
 79            env: None,
 80            arguments: server_binary_arguments(&server_path),
 81        })
 82    }
 83
 84    async fn cached_server_binary(
 85        &self,
 86        container_dir: PathBuf,
 87        _: &dyn LspAdapterDelegate,
 88    ) -> Option<LanguageServerBinary> {
 89        get_cached_server_binary(container_dir, &*self.node).await
 90    }
 91
 92    async fn installation_test_binary(
 93        &self,
 94        container_dir: PathBuf,
 95    ) -> Option<LanguageServerBinary> {
 96        get_cached_server_binary(container_dir, &*self.node).await
 97    }
 98
 99    async fn initialization_options(
100        self: Arc<Self>,
101        _: &Arc<dyn LspAdapterDelegate>,
102    ) -> Result<Option<serde_json::Value>> {
103        Ok(Some(json!({
104            "provideFormatter": true,
105            "userLanguages": {
106                "html": "html",
107                "css": "css",
108                "javascript": "javascript",
109                "typescriptreact": "typescriptreact",
110            },
111        })))
112    }
113
114    async fn workspace_configuration(
115        self: Arc<Self>,
116        _: &Arc<dyn LspAdapterDelegate>,
117        cx: &mut AsyncAppContext,
118    ) -> Result<Value> {
119        let tailwind_user_settings = cx.update(|cx| {
120            ProjectSettings::get_global(cx)
121                .lsp
122                .get(Self::SERVER_NAME)
123                .and_then(|s| s.settings.clone())
124                .unwrap_or_default()
125        })?;
126
127        // We need to set this to null if it's not set, because tailwindcss-languageserver
128        // will check whether it's an object and if it is (even if it's empty) it will
129        // ignore the `userLanguages` from the initialization options.
130        let include_languages = tailwind_user_settings
131            .get("includeLanguages")
132            .cloned()
133            .unwrap_or(Value::Null);
134
135        let experimental = tailwind_user_settings
136            .get("experimental")
137            .cloned()
138            .unwrap_or_else(|| json!([]));
139
140        Ok(json!({
141            "tailwindCSS": {
142                "emmetCompletions": true,
143                "includeLanguages": include_languages,
144                "experimental": experimental,
145            }
146        }))
147    }
148
149    fn language_ids(&self) -> HashMap<String, String> {
150        HashMap::from_iter([
151            ("Astro".to_string(), "astro".to_string()),
152            ("HTML".to_string(), "html".to_string()),
153            ("CSS".to_string(), "css".to_string()),
154            ("JavaScript".to_string(), "javascript".to_string()),
155            ("TSX".to_string(), "typescriptreact".to_string()),
156            ("Svelte".to_string(), "svelte".to_string()),
157            ("Elixir".to_string(), "phoenix-heex".to_string()),
158            ("HEEX".to_string(), "phoenix-heex".to_string()),
159            ("ERB".to_string(), "erb".to_string()),
160            ("PHP".to_string(), "php".to_string()),
161            ("Vue.js".to_string(), "vue".to_string()),
162        ])
163    }
164}
165
166async fn get_cached_server_binary(
167    container_dir: PathBuf,
168    node: &dyn NodeRuntime,
169) -> Option<LanguageServerBinary> {
170    maybe!(async {
171        let mut last_version_dir = None;
172        let mut entries = fs::read_dir(&container_dir).await?;
173        while let Some(entry) = entries.next().await {
174            let entry = entry?;
175            if entry.file_type().await?.is_dir() {
176                last_version_dir = Some(entry.path());
177            }
178        }
179        let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
180        let server_path = last_version_dir.join(SERVER_PATH);
181        if server_path.exists() {
182            Ok(LanguageServerBinary {
183                path: node.binary_path().await?,
184                env: None,
185                arguments: server_binary_arguments(&server_path),
186            })
187        } else {
188            Err(anyhow!(
189                "missing executable in directory {:?}",
190                last_version_dir
191            ))
192        }
193    })
194    .await
195    .log_err()
196}