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