1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use futures::StreamExt;
5use gpui::AppContext;
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::{async_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 fn initialization_options(&self) -> Option<serde_json::Value> {
96 Some(json!({
97 "provideFormatter": true,
98 "userLanguages": {
99 "html": "html",
100 "css": "css",
101 "javascript": "javascript",
102 "typescriptreact": "typescriptreact",
103 },
104 }))
105 }
106
107 fn workspace_configuration(&self, _workspace_root: &Path, _: &mut AppContext) -> Value {
108 json!({
109 "tailwindCSS": {
110 "emmetCompletions": true,
111 }
112 })
113 }
114
115 fn language_ids(&self) -> HashMap<String, String> {
116 HashMap::from_iter([
117 ("Astro".to_string(), "astro".to_string()),
118 ("HTML".to_string(), "html".to_string()),
119 ("CSS".to_string(), "css".to_string()),
120 ("JavaScript".to_string(), "javascript".to_string()),
121 ("TSX".to_string(), "typescriptreact".to_string()),
122 ("Svelte".to_string(), "svelte".to_string()),
123 ("Elixir".to_string(), "phoenix-heex".to_string()),
124 ("HEEX".to_string(), "phoenix-heex".to_string()),
125 ("ERB".to_string(), "erb".to_string()),
126 ("PHP".to_string(), "php".to_string()),
127 ])
128 }
129
130 fn prettier_plugins(&self) -> &[&'static str] {
131 &["prettier-plugin-tailwindcss"]
132 }
133}
134
135async fn get_cached_server_binary(
136 container_dir: PathBuf,
137 node: &dyn NodeRuntime,
138) -> Option<LanguageServerBinary> {
139 async_maybe!({
140 let mut last_version_dir = None;
141 let mut entries = fs::read_dir(&container_dir).await?;
142 while let Some(entry) = entries.next().await {
143 let entry = entry?;
144 if entry.file_type().await?.is_dir() {
145 last_version_dir = Some(entry.path());
146 }
147 }
148 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
149 let server_path = last_version_dir.join(SERVER_PATH);
150 if server_path.exists() {
151 Ok(LanguageServerBinary {
152 path: node.binary_path().await?,
153 env: None,
154 arguments: server_binary_arguments(&server_path),
155 })
156 } else {
157 Err(anyhow!(
158 "missing executable in directory {:?}",
159 last_version_dir
160 ))
161 }
162 })
163 .await
164 .log_err()
165}