1use anyhow::{Context as _, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::AsyncApp;
6use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
7use lsp::{LanguageServerBinary, LanguageServerName};
8use node_runtime::{NodeRuntime, VersionStrategy};
9use project::lsp_store::language_server_settings;
10use serde_json::{Value, json};
11use smol::fs;
12use std::{
13 ffi::OsString,
14 path::{Path, PathBuf},
15 sync::Arc,
16};
17use util::{ResultExt, maybe};
18
19#[cfg(target_os = "windows")]
20const SERVER_PATH: &str =
21 "node_modules/@tailwindcss/language-server/bin/tailwindcss-language-server";
22#[cfg(not(target_os = "windows"))]
23const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
24
25fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
26 vec![server_path.into(), "--stdio".into()]
27}
28
29pub struct TailwindLspAdapter {
30 node: NodeRuntime,
31}
32
33impl TailwindLspAdapter {
34 const SERVER_NAME: LanguageServerName =
35 LanguageServerName::new_static("tailwindcss-language-server");
36 const PACKAGE_NAME: &str = "@tailwindcss/language-server";
37
38 pub const fn new(node: NodeRuntime) -> Self {
39 TailwindLspAdapter { node }
40 }
41}
42
43impl LspInstaller for TailwindLspAdapter {
44 type BinaryVersion = String;
45
46 async fn fetch_latest_server_version(
47 &self,
48 _: &dyn LspAdapterDelegate,
49 _: bool,
50 _: &mut AsyncApp,
51 ) -> Result<String> {
52 self.node
53 .npm_package_latest_version(Self::PACKAGE_NAME)
54 .await
55 }
56
57 async fn check_if_user_installed(
58 &self,
59 delegate: &dyn LspAdapterDelegate,
60 _: Option<Toolchain>,
61 _: &AsyncApp,
62 ) -> Option<LanguageServerBinary> {
63 let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
64 let env = delegate.shell_env().await;
65
66 Some(LanguageServerBinary {
67 path,
68 env: Some(env),
69 arguments: vec!["--stdio".into()],
70 })
71 }
72
73 async fn fetch_server_binary(
74 &self,
75 latest_version: String,
76 container_dir: PathBuf,
77 _: &dyn LspAdapterDelegate,
78 ) -> Result<LanguageServerBinary> {
79 let server_path = container_dir.join(SERVER_PATH);
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: &String,
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 "userLanguages": {
146 "html": "html",
147 "css": "css",
148 "javascript": "javascript",
149 "typescript": "typescript",
150 "typescriptreact": "typescriptreact",
151 },
152 })))
153 }
154
155 async fn workspace_configuration(
156 self: Arc<Self>,
157 delegate: &Arc<dyn LspAdapterDelegate>,
158 _: Option<Toolchain>,
159 cx: &mut AsyncApp,
160 ) -> Result<Value> {
161 let mut tailwind_user_settings = cx.update(|cx| {
162 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
163 .and_then(|s| s.settings.clone())
164 .unwrap_or_default()
165 })?;
166
167 if tailwind_user_settings.get("emmetCompletions").is_none() {
168 tailwind_user_settings["emmetCompletions"] = Value::Bool(true);
169 }
170
171 Ok(json!({
172 "tailwindCSS": tailwind_user_settings,
173 }))
174 }
175
176 fn language_ids(&self) -> HashMap<LanguageName, String> {
177 HashMap::from_iter([
178 (LanguageName::new("Astro"), "astro".to_string()),
179 (LanguageName::new("HTML"), "html".to_string()),
180 (LanguageName::new("CSS"), "css".to_string()),
181 (LanguageName::new("JavaScript"), "javascript".to_string()),
182 (LanguageName::new("TypeScript"), "typescript".to_string()),
183 (LanguageName::new("TSX"), "typescriptreact".to_string()),
184 (LanguageName::new("Svelte"), "svelte".to_string()),
185 (LanguageName::new("Elixir"), "phoenix-heex".to_string()),
186 (LanguageName::new("HEEX"), "phoenix-heex".to_string()),
187 (LanguageName::new("ERB"), "erb".to_string()),
188 (LanguageName::new("HTML+ERB"), "erb".to_string()),
189 (LanguageName::new("HTML/ERB"), "erb".to_string()),
190 (LanguageName::new("PHP"), "php".to_string()),
191 (LanguageName::new("Vue.js"), "vue".to_string()),
192 ])
193 }
194}
195
196async fn get_cached_server_binary(
197 container_dir: PathBuf,
198 node: &NodeRuntime,
199) -> Option<LanguageServerBinary> {
200 maybe!(async {
201 let mut last_version_dir = None;
202 let mut entries = fs::read_dir(&container_dir).await?;
203 while let Some(entry) = entries.next().await {
204 let entry = entry?;
205 if entry.file_type().await?.is_dir() {
206 last_version_dir = Some(entry.path());
207 }
208 }
209 let last_version_dir = last_version_dir.context("no cached binary")?;
210 let server_path = last_version_dir.join(SERVER_PATH);
211 anyhow::ensure!(
212 server_path.exists(),
213 "missing executable in directory {last_version_dir:?}"
214 );
215 Ok(LanguageServerBinary {
216 path: node.binary_path().await?,
217 env: None,
218 arguments: server_binary_arguments(&server_path),
219 })
220 })
221 .await
222 .log_err()
223}