1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::AppContext;
6use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
7use lsp::LanguageServerBinary;
8use node_runtime::NodeRuntime;
9use serde_json::{json, Value};
10use smol::fs;
11use std::{
12 any::Any,
13 ffi::OsString,
14 path::{Path, PathBuf},
15 sync::Arc,
16};
17use util::{async_maybe, ResultExt};
18
19const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
20
21fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
22 vec![server_path.into(), "--stdio".into()]
23}
24
25pub struct TailwindLspAdapter {
26 node: Arc<dyn NodeRuntime>,
27}
28
29impl TailwindLspAdapter {
30 pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
31 TailwindLspAdapter { node }
32 }
33}
34
35#[async_trait]
36impl LspAdapter for TailwindLspAdapter {
37 fn name(&self) -> LanguageServerName {
38 LanguageServerName("tailwindcss-language-server".into())
39 }
40
41 async fn fetch_latest_server_version(
42 &self,
43 _: &dyn LspAdapterDelegate,
44 ) -> Result<Box<dyn 'static + Any + Send>> {
45 Ok(Box::new(
46 self.node
47 .npm_package_latest_version("@tailwindcss/language-server")
48 .await?,
49 ) as Box<_>)
50 }
51
52 async fn fetch_server_binary(
53 &self,
54 version: Box<dyn 'static + Send + Any>,
55 container_dir: PathBuf,
56 _: &dyn LspAdapterDelegate,
57 ) -> Result<LanguageServerBinary> {
58 let version = version.downcast::<String>().unwrap();
59 let server_path = container_dir.join(SERVER_PATH);
60
61 if fs::metadata(&server_path).await.is_err() {
62 self.node
63 .npm_install_packages(
64 &container_dir,
65 &[("@tailwindcss/language-server", version.as_str())],
66 )
67 .await?;
68 }
69
70 Ok(LanguageServerBinary {
71 path: self.node.binary_path().await?,
72 env: None,
73 arguments: server_binary_arguments(&server_path),
74 })
75 }
76
77 async fn cached_server_binary(
78 &self,
79 container_dir: PathBuf,
80 _: &dyn LspAdapterDelegate,
81 ) -> Option<LanguageServerBinary> {
82 get_cached_server_binary(container_dir, &*self.node).await
83 }
84
85 async fn installation_test_binary(
86 &self,
87 container_dir: PathBuf,
88 ) -> Option<LanguageServerBinary> {
89 get_cached_server_binary(container_dir, &*self.node).await
90 }
91
92 fn initialization_options(&self) -> Option<serde_json::Value> {
93 Some(json!({
94 "provideFormatter": true,
95 "userLanguages": {
96 "html": "html",
97 "css": "css",
98 "javascript": "javascript",
99 "typescriptreact": "typescriptreact",
100 },
101 }))
102 }
103
104 fn workspace_configuration(&self, _workspace_root: &Path, _: &mut AppContext) -> Value {
105 json!({
106 "tailwindCSS": {
107 "emmetCompletions": true,
108 }
109 })
110 }
111
112 fn language_ids(&self) -> HashMap<String, String> {
113 HashMap::from_iter([
114 ("Astro".to_string(), "astro".to_string()),
115 ("HTML".to_string(), "html".to_string()),
116 ("CSS".to_string(), "css".to_string()),
117 ("JavaScript".to_string(), "javascript".to_string()),
118 ("TSX".to_string(), "typescriptreact".to_string()),
119 ("Svelte".to_string(), "svelte".to_string()),
120 ("Elixir".to_string(), "phoenix-heex".to_string()),
121 ("HEEX".to_string(), "phoenix-heex".to_string()),
122 ("ERB".to_string(), "erb".to_string()),
123 ("PHP".to_string(), "php".to_string()),
124 ])
125 }
126
127 fn prettier_plugins(&self) -> &[&'static str] {
128 &["prettier-plugin-tailwindcss"]
129 }
130}
131
132async fn get_cached_server_binary(
133 container_dir: PathBuf,
134 node: &dyn NodeRuntime,
135) -> Option<LanguageServerBinary> {
136 async_maybe!({
137 let mut last_version_dir = None;
138 let mut entries = fs::read_dir(&container_dir).await?;
139 while let Some(entry) = entries.next().await {
140 let entry = entry?;
141 if entry.file_type().await?.is_dir() {
142 last_version_dir = Some(entry.path());
143 }
144 }
145 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
146 let server_path = last_version_dir.join(SERVER_PATH);
147 if server_path.exists() {
148 Ok(LanguageServerBinary {
149 path: node.binary_path().await?,
150 env: None,
151 arguments: server_binary_arguments(&server_path),
152 })
153 } else {
154 Err(anyhow!(
155 "missing executable in directory {:?}",
156 last_version_dir
157 ))
158 }
159 })
160 .await
161 .log_err()
162}