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::{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 fn workspace_configuration(&self, _workspace_root: &Path, _: &mut AppContext) -> Value {
111 json!({
112 "tailwindCSS": {
113 "emmetCompletions": true,
114 }
115 })
116 }
117
118 fn language_ids(&self) -> HashMap<String, String> {
119 HashMap::from_iter([
120 ("Astro".to_string(), "astro".to_string()),
121 ("HTML".to_string(), "html".to_string()),
122 ("CSS".to_string(), "css".to_string()),
123 ("JavaScript".to_string(), "javascript".to_string()),
124 ("TSX".to_string(), "typescriptreact".to_string()),
125 ("Svelte".to_string(), "svelte".to_string()),
126 ("Elixir".to_string(), "phoenix-heex".to_string()),
127 ("HEEX".to_string(), "phoenix-heex".to_string()),
128 ("ERB".to_string(), "erb".to_string()),
129 ("PHP".to_string(), "php".to_string()),
130 ("Vue.js".to_string(), "vue".to_string()),
131 ])
132 }
133}
134
135async fn get_cached_server_binary(
136 container_dir: PathBuf,
137 node: &dyn NodeRuntime,
138) -> Option<LanguageServerBinary> {
139 maybe!(async {
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}