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 ) -> Result<Option<serde_json::Value>> {
143 Ok(Some(json!({
144 "provideFormatter": true,
145 })))
146 }
147
148 async fn workspace_configuration(
149 self: Arc<Self>,
150 delegate: &Arc<dyn LspAdapterDelegate>,
151 _: Option<Toolchain>,
152 _: Option<Uri>,
153 cx: &mut AsyncApp,
154 ) -> Result<Value> {
155 let mut tailwind_user_settings = cx.update(|cx| {
156 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
157 .and_then(|s| s.settings.clone())
158 .unwrap_or_default()
159 })?;
160
161 if tailwind_user_settings.get("emmetCompletions").is_none() {
162 tailwind_user_settings["emmetCompletions"] = Value::Bool(true);
163 }
164
165 if tailwind_user_settings.get("includeLanguages").is_none() {
166 tailwind_user_settings["includeLanguages"] = json!({
167 "html": "html",
168 "css": "css",
169 "javascript": "javascript",
170 "typescript": "typescript",
171 "typescriptreact": "typescriptreact",
172 });
173 }
174
175 Ok(json!({
176 "tailwindCSS": tailwind_user_settings
177 }))
178 }
179
180 fn language_ids(&self) -> HashMap<LanguageName, String> {
181 HashMap::from_iter([
182 (LanguageName::new_static("Astro"), "astro".to_string()),
183 (LanguageName::new_static("HTML"), "html".to_string()),
184 (LanguageName::new_static("Gleam"), "html".to_string()),
185 (LanguageName::new_static("CSS"), "css".to_string()),
186 (
187 LanguageName::new_static("JavaScript"),
188 "javascript".to_string(),
189 ),
190 (
191 LanguageName::new_static("TypeScript"),
192 "typescript".to_string(),
193 ),
194 (
195 LanguageName::new_static("TSX"),
196 "typescriptreact".to_string(),
197 ),
198 (LanguageName::new_static("Svelte"), "svelte".to_string()),
199 (
200 LanguageName::new_static("Elixir"),
201 "phoenix-heex".to_string(),
202 ),
203 (LanguageName::new_static("HEEX"), "phoenix-heex".to_string()),
204 (LanguageName::new_static("ERB"), "erb".to_string()),
205 (LanguageName::new_static("HTML+ERB"), "erb".to_string()),
206 (LanguageName::new_static("PHP"), "php".to_string()),
207 (LanguageName::new_static("Vue.js"), "vue".to_string()),
208 ])
209 }
210}
211
212async fn get_cached_server_binary(
213 container_dir: PathBuf,
214 node: &NodeRuntime,
215) -> Option<LanguageServerBinary> {
216 maybe!(async {
217 let server_path = container_dir.join(SERVER_PATH);
218 anyhow::ensure!(
219 server_path.exists(),
220 "missing executable in directory {server_path:?}"
221 );
222 Ok(LanguageServerBinary {
223 path: node.binary_path().await?,
224 env: None,
225 arguments: server_binary_arguments(&server_path),
226 })
227 })
228 .await
229 .log_err()
230}