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