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