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