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: &'static 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]
36impl LspAdapter for TailwindLspAdapter {
37 fn name(&self) -> LanguageServerName {
38 LanguageServerName("tailwindcss-language-server".into())
39 }
40
41 fn short_name(&self) -> &'static str {
42 "tailwind"
43 }
44
45 async fn fetch_latest_server_version(
46 &self,
47 _: &dyn LspAdapterDelegate,
48 ) -> Result<Box<dyn 'static + Any + Send>> {
49 Ok(Box::new(
50 self.node
51 .npm_package_latest_version("@tailwindcss/language-server")
52 .await?,
53 ) as Box<_>)
54 }
55
56 async fn fetch_server_binary(
57 &self,
58 version: Box<dyn 'static + Send + Any>,
59 container_dir: PathBuf,
60 _: &dyn LspAdapterDelegate,
61 ) -> Result<LanguageServerBinary> {
62 let version = version.downcast::<String>().unwrap();
63 let server_path = container_dir.join(SERVER_PATH);
64
65 if fs::metadata(&server_path).await.is_err() {
66 self.node
67 .npm_install_packages(
68 &container_dir,
69 &[("@tailwindcss/language-server", version.as_str())],
70 )
71 .await?;
72 }
73
74 Ok(LanguageServerBinary {
75 path: self.node.binary_path().await?,
76 env: None,
77 arguments: server_binary_arguments(&server_path),
78 })
79 }
80
81 async fn cached_server_binary(
82 &self,
83 container_dir: PathBuf,
84 _: &dyn LspAdapterDelegate,
85 ) -> Option<LanguageServerBinary> {
86 get_cached_server_binary(container_dir, &*self.node).await
87 }
88
89 async fn installation_test_binary(
90 &self,
91 container_dir: PathBuf,
92 ) -> Option<LanguageServerBinary> {
93 get_cached_server_binary(container_dir, &*self.node).await
94 }
95
96 fn initialization_options(&self) -> Option<serde_json::Value> {
97 Some(json!({
98 "provideFormatter": true,
99 "userLanguages": {
100 "html": "html",
101 "css": "css",
102 "javascript": "javascript",
103 "typescriptreact": "typescriptreact",
104 },
105 }))
106 }
107
108 fn workspace_configuration(&self, _workspace_root: &Path, _: &mut AppContext) -> Value {
109 json!({
110 "tailwindCSS": {
111 "emmetCompletions": true,
112 }
113 })
114 }
115
116 fn language_ids(&self) -> HashMap<String, String> {
117 HashMap::from_iter([
118 ("Astro".to_string(), "astro".to_string()),
119 ("HTML".to_string(), "html".to_string()),
120 ("CSS".to_string(), "css".to_string()),
121 ("JavaScript".to_string(), "javascript".to_string()),
122 ("TSX".to_string(), "typescriptreact".to_string()),
123 ("Svelte".to_string(), "svelte".to_string()),
124 ("Elixir".to_string(), "phoenix-heex".to_string()),
125 ("HEEX".to_string(), "phoenix-heex".to_string()),
126 ("ERB".to_string(), "erb".to_string()),
127 ("PHP".to_string(), "php".to_string()),
128 ])
129 }
130
131 fn prettier_plugins(&self) -> &[&'static str] {
132 &["prettier-plugin-tailwindcss"]
133 }
134}
135
136async fn get_cached_server_binary(
137 container_dir: PathBuf,
138 node: &dyn NodeRuntime,
139) -> Option<LanguageServerBinary> {
140 async_maybe!({
141 let mut last_version_dir = None;
142 let mut entries = fs::read_dir(&container_dir).await?;
143 while let Some(entry) = entries.next().await {
144 let entry = entry?;
145 if entry.file_type().await?.is_dir() {
146 last_version_dir = Some(entry.path());
147 }
148 }
149 let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
150 let server_path = last_version_dir.join(SERVER_PATH);
151 if server_path.exists() {
152 Ok(LanguageServerBinary {
153 path: node.binary_path().await?,
154 env: None,
155 arguments: server_binary_arguments(&server_path),
156 })
157 } else {
158 Err(anyhow!(
159 "missing executable in directory {:?}",
160 last_version_dir
161 ))
162 }
163 })
164 .await
165 .log_err()
166}