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 serde_json::{json, Value};
10use smol::fs;
11use std::{
12 any::Any,
13 ffi::OsString,
14 path::{Path, PathBuf},
15 sync::Arc,
16};
17use util::{maybe, ResultExt};
18
19const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server";
20
21fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
22 vec![server_path.into(), "--stdio".into()]
23}
24
25pub struct TailwindLspAdapter {
26 node: Arc<dyn NodeRuntime>,
27}
28
29impl TailwindLspAdapter {
30 pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
31 TailwindLspAdapter { node }
32 }
33}
34
35#[async_trait(?Send)]
36impl LspAdapter for TailwindLspAdapter {
37 fn name(&self) -> LanguageServerName {
38 LanguageServerName("tailwindcss-language-server".into())
39 }
40
41 async fn fetch_latest_server_version(
42 &self,
43 _: &dyn LspAdapterDelegate,
44 ) -> Result<Box<dyn 'static + Any + Send>> {
45 Ok(Box::new(
46 self.node
47 .npm_package_latest_version("@tailwindcss/language-server")
48 .await?,
49 ) as Box<_>)
50 }
51
52 async fn fetch_server_binary(
53 &self,
54 latest_version: Box<dyn 'static + Send + Any>,
55 container_dir: PathBuf,
56 _: &dyn LspAdapterDelegate,
57 ) -> Result<LanguageServerBinary> {
58 let latest_version = latest_version.downcast::<String>().unwrap();
59 let server_path = container_dir.join(SERVER_PATH);
60 let package_name = "@tailwindcss/language-server";
61
62 let should_install_language_server = self
63 .node
64 .should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
65 .await;
66
67 if should_install_language_server {
68 self.node
69 .npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
70 .await?;
71 }
72
73 Ok(LanguageServerBinary {
74 path: self.node.binary_path().await?,
75 env: None,
76 arguments: server_binary_arguments(&server_path),
77 })
78 }
79
80 async fn cached_server_binary(
81 &self,
82 container_dir: PathBuf,
83 _: &dyn LspAdapterDelegate,
84 ) -> Option<LanguageServerBinary> {
85 get_cached_server_binary(container_dir, &*self.node).await
86 }
87
88 async fn installation_test_binary(
89 &self,
90 container_dir: PathBuf,
91 ) -> Option<LanguageServerBinary> {
92 get_cached_server_binary(container_dir, &*self.node).await
93 }
94
95 async fn initialization_options(
96 self: Arc<Self>,
97 _: &Arc<dyn LspAdapterDelegate>,
98 ) -> Result<Option<serde_json::Value>> {
99 Ok(Some(json!({
100 "provideFormatter": true,
101 "userLanguages": {
102 "html": "html",
103 "css": "css",
104 "javascript": "javascript",
105 "typescriptreact": "typescriptreact",
106 },
107 })))
108 }
109
110 async fn workspace_configuration(
111 self: Arc<Self>,
112 _: &Arc<dyn LspAdapterDelegate>,
113 _cx: &mut AsyncAppContext,
114 ) -> Result<Value> {
115 Ok(json!({
116 "tailwindCSS": {
117 "emmetCompletions": true,
118 }
119 }))
120 }
121
122 fn language_ids(&self) -> HashMap<String, String> {
123 HashMap::from_iter([
124 ("Astro".to_string(), "astro".to_string()),
125 ("HTML".to_string(), "html".to_string()),
126 ("CSS".to_string(), "css".to_string()),
127 ("JavaScript".to_string(), "javascript".to_string()),
128 ("TSX".to_string(), "typescriptreact".to_string()),
129 ("Svelte".to_string(), "svelte".to_string()),
130 ("Elixir".to_string(), "phoenix-heex".to_string()),
131 ("HEEX".to_string(), "phoenix-heex".to_string()),
132 ("ERB".to_string(), "erb".to_string()),
133 ("PHP".to_string(), "php".to_string()),
134 ("Vue.js".to_string(), "vue".to_string()),
135 ])
136 }
137}
138
139async fn get_cached_server_binary(
140 container_dir: PathBuf,
141 node: &dyn NodeRuntime,
142) -> Option<LanguageServerBinary> {
143 maybe!(async {
144 let mut last_version_dir = None;
145 let mut entries = fs::read_dir(&container_dir).await?;
146 while let Some(entry) = entries.next().await {
147 let entry = entry?;
148 if entry.file_type().await?.is_dir() {
149 last_version_dir = Some(entry.path());
150 }
151 }
152 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
153 let server_path = last_version_dir.join(SERVER_PATH);
154 if server_path.exists() {
155 Ok(LanguageServerBinary {
156 path: node.binary_path().await?,
157 env: None,
158 arguments: server_binary_arguments(&server_path),
159 })
160 } else {
161 Err(anyhow!(
162 "missing executable in directory {:?}",
163 last_version_dir
164 ))
165 }
166 })
167 .await
168 .log_err()
169}