1use anyhow::Result;
2use async_trait::async_trait;
3use collections::HashMap;
4use gpui::AsyncApp;
5use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
6use lsp::{LanguageServerBinary, LanguageServerName, Uri};
7use node_runtime::{NodeRuntime, VersionStrategy};
8use project::lsp_store::language_server_settings;
9use semver::Version;
10use serde_json::{Value, json};
11use std::{
12 ffi::OsString,
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16use util::{ResultExt, maybe};
17
18#[cfg(target_os = "windows")]
19const SERVER_PATH: &str =
20 "node_modules/@tailwindcss/language-server/bin/tailwindcss-language-server";
21#[cfg(not(target_os = "windows"))]
22const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
23
24fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
25 vec![server_path.into(), "--stdio".into()]
26}
27
28pub struct TailwindLspAdapter {
29 node: NodeRuntime,
30}
31
32impl TailwindLspAdapter {
33 const SERVER_NAME: LanguageServerName =
34 LanguageServerName::new_static("tailwindcss-language-server");
35 const PACKAGE_NAME: &str = "@tailwindcss/language-server";
36
37 pub fn new(node: NodeRuntime) -> Self {
38 TailwindLspAdapter { node }
39 }
40}
41
42impl LspInstaller for TailwindLspAdapter {
43 type BinaryVersion = Version;
44
45 async fn fetch_latest_server_version(
46 &self,
47 _: &dyn LspAdapterDelegate,
48 _: bool,
49 _: &mut AsyncApp,
50 ) -> Result<Self::BinaryVersion> {
51 self.node
52 .npm_package_latest_version(Self::PACKAGE_NAME)
53 .await
54 }
55
56 async fn check_if_user_installed(
57 &self,
58 delegate: &dyn LspAdapterDelegate,
59 _: Option<Toolchain>,
60 _: &AsyncApp,
61 ) -> Option<LanguageServerBinary> {
62 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
63 let env = delegate.shell_env().await;
64
65 Some(LanguageServerBinary {
66 path,
67 env: Some(env),
68 arguments: vec!["--stdio".into()],
69 })
70 }
71
72 async fn fetch_server_binary(
73 &self,
74 latest_version: Self::BinaryVersion,
75 container_dir: PathBuf,
76 _: &dyn LspAdapterDelegate,
77 ) -> Result<LanguageServerBinary> {
78 let server_path = container_dir.join(SERVER_PATH);
79 let latest_version = latest_version.to_string();
80
81 self.node
82 .npm_install_packages(
83 &container_dir,
84 &[(Self::PACKAGE_NAME, latest_version.as_str())],
85 )
86 .await?;
87
88 Ok(LanguageServerBinary {
89 path: self.node.binary_path().await?,
90 env: None,
91 arguments: server_binary_arguments(&server_path),
92 })
93 }
94
95 async fn check_if_version_installed(
96 &self,
97 version: &Self::BinaryVersion,
98 container_dir: &PathBuf,
99 _: &dyn LspAdapterDelegate,
100 ) -> Option<LanguageServerBinary> {
101 let server_path = container_dir.join(SERVER_PATH);
102
103 let should_install_language_server = self
104 .node
105 .should_install_npm_package(
106 Self::PACKAGE_NAME,
107 &server_path,
108 container_dir,
109 VersionStrategy::Latest(version),
110 )
111 .await;
112
113 if should_install_language_server {
114 None
115 } else {
116 Some(LanguageServerBinary {
117 path: self.node.binary_path().await.ok()?,
118 env: None,
119 arguments: server_binary_arguments(&server_path),
120 })
121 }
122 }
123
124 async fn cached_server_binary(
125 &self,
126 container_dir: PathBuf,
127 _: &dyn LspAdapterDelegate,
128 ) -> Option<LanguageServerBinary> {
129 get_cached_server_binary(container_dir, &self.node).await
130 }
131}
132
133#[async_trait(?Send)]
134impl LspAdapter for TailwindLspAdapter {
135 fn name(&self) -> LanguageServerName {
136 Self::SERVER_NAME
137 }
138
139 async fn initialization_options(
140 self: Arc<Self>,
141 _: &Arc<dyn LspAdapterDelegate>,
142 _: &mut AsyncApp,
143 ) -> Result<Option<serde_json::Value>> {
144 Ok(Some(json!({
145 "provideFormatter": true,
146 })))
147 }
148
149 async fn workspace_configuration(
150 self: Arc<Self>,
151 delegate: &Arc<dyn LspAdapterDelegate>,
152 _: Option<Toolchain>,
153 _: Option<Uri>,
154 cx: &mut AsyncApp,
155 ) -> Result<Value> {
156 let mut tailwind_user_settings = cx.update(|cx| {
157 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
158 .and_then(|s| s.settings.clone())
159 .unwrap_or_default()
160 });
161
162 if tailwind_user_settings.get("emmetCompletions").is_none() {
163 tailwind_user_settings["emmetCompletions"] = Value::Bool(true);
164 }
165
166 if tailwind_user_settings.get("includeLanguages").is_none() {
167 tailwind_user_settings["includeLanguages"] = json!({
168 "html": "html",
169 "css": "css",
170 "javascript": "javascript",
171 "typescript": "typescript",
172 "typescriptreact": "typescriptreact",
173 });
174 }
175
176 Ok(json!({
177 "tailwindCSS": tailwind_user_settings
178 }))
179 }
180
181 fn language_ids(&self) -> HashMap<LanguageName, String> {
182 HashMap::from_iter([
183 (LanguageName::new_static("Astro"), "astro".to_string()),
184 (LanguageName::new_static("HTML"), "html".to_string()),
185 (LanguageName::new_static("Gleam"), "html".to_string()),
186 (LanguageName::new_static("CSS"), "css".to_string()),
187 (
188 LanguageName::new_static("JavaScript"),
189 "javascript".to_string(),
190 ),
191 (
192 LanguageName::new_static("TypeScript"),
193 "typescript".to_string(),
194 ),
195 (
196 LanguageName::new_static("TSX"),
197 "typescriptreact".to_string(),
198 ),
199 (LanguageName::new_static("Svelte"), "svelte".to_string()),
200 (
201 LanguageName::new_static("Elixir"),
202 "phoenix-heex".to_string(),
203 ),
204 (LanguageName::new_static("HEEX"), "phoenix-heex".to_string()),
205 (LanguageName::new_static("ERB"), "erb".to_string()),
206 (LanguageName::new_static("HTML+ERB"), "erb".to_string()),
207 (LanguageName::new_static("PHP"), "php".to_string()),
208 (LanguageName::new_static("Vue.js"), "vue".to_string()),
209 ])
210 }
211}
212
213async fn get_cached_server_binary(
214 container_dir: PathBuf,
215 node: &NodeRuntime,
216) -> Option<LanguageServerBinary> {
217 maybe!(async {
218 let server_path = container_dir.join(SERVER_PATH);
219 anyhow::ensure!(
220 server_path.exists(),
221 "missing executable in directory {server_path:?}"
222 );
223 Ok(LanguageServerBinary {
224 path: node.binary_path().await?,
225 env: None,
226 arguments: server_binary_arguments(&server_path),
227 })
228 })
229 .await
230 .log_err()
231}