1use anyhow::{anyhow, Result};
2use async_trait::async_trait;
3use collections::HashMap;
4use gpui::AsyncAppContext;
5use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
6use lsp::{CodeActionKind, LanguageServerBinary};
7use node_runtime::NodeRuntime;
8use project::project_settings::ProjectSettings;
9use serde_json::{json, Value};
10use settings::Settings;
11use std::{
12 any::Any,
13 ffi::OsString,
14 path::{Path, PathBuf},
15 sync::Arc,
16};
17use util::{maybe, ResultExt};
18
19fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
20 vec![server_path.into(), "--stdio".into()]
21}
22
23pub struct VtslsLspAdapter {
24 node: Arc<dyn NodeRuntime>,
25}
26
27impl VtslsLspAdapter {
28 const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
29
30 pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
31 VtslsLspAdapter { node }
32 }
33 async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
34 let is_yarn = adapter
35 .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
36 .await
37 .is_ok();
38
39 if is_yarn {
40 ".yarn/sdks/typescript/lib"
41 } else {
42 "node_modules/typescript/lib"
43 }
44 }
45}
46
47struct TypeScriptVersions {
48 typescript_version: String,
49 server_version: String,
50}
51
52const SERVER_NAME: &'static str = "vtsls";
53#[async_trait(?Send)]
54impl LspAdapter for VtslsLspAdapter {
55 fn name(&self) -> LanguageServerName {
56 LanguageServerName(SERVER_NAME.into())
57 }
58
59 async fn fetch_latest_server_version(
60 &self,
61 _: &dyn LspAdapterDelegate,
62 ) -> Result<Box<dyn 'static + Send + Any>> {
63 Ok(Box::new(TypeScriptVersions {
64 typescript_version: self.node.npm_package_latest_version("typescript").await?,
65 server_version: self
66 .node
67 .npm_package_latest_version("@vtsls/language-server")
68 .await?,
69 }) as Box<_>)
70 }
71
72 async fn fetch_server_binary(
73 &self,
74 latest_version: Box<dyn 'static + Send + Any>,
75 container_dir: PathBuf,
76 _: &dyn LspAdapterDelegate,
77 ) -> Result<LanguageServerBinary> {
78 let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
79 let server_path = container_dir.join(Self::SERVER_PATH);
80 let package_name = "typescript";
81
82 let should_install_language_server = self
83 .node
84 .should_install_npm_package(
85 package_name,
86 &server_path,
87 &container_dir,
88 latest_version.typescript_version.as_str(),
89 )
90 .await;
91
92 if should_install_language_server {
93 self.node
94 .npm_install_packages(
95 &container_dir,
96 &[
97 (package_name, latest_version.typescript_version.as_str()),
98 (
99 "@vtsls/language-server",
100 latest_version.server_version.as_str(),
101 ),
102 ],
103 )
104 .await?;
105 }
106
107 Ok(LanguageServerBinary {
108 path: self.node.binary_path().await?,
109 env: None,
110 arguments: typescript_server_binary_arguments(&server_path),
111 })
112 }
113
114 async fn cached_server_binary(
115 &self,
116 container_dir: PathBuf,
117 _: &dyn LspAdapterDelegate,
118 ) -> Option<LanguageServerBinary> {
119 get_cached_ts_server_binary(container_dir, &*self.node).await
120 }
121
122 async fn installation_test_binary(
123 &self,
124 container_dir: PathBuf,
125 ) -> Option<LanguageServerBinary> {
126 get_cached_ts_server_binary(container_dir, &*self.node).await
127 }
128
129 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
130 Some(vec![
131 CodeActionKind::QUICKFIX,
132 CodeActionKind::REFACTOR,
133 CodeActionKind::REFACTOR_EXTRACT,
134 CodeActionKind::SOURCE,
135 ])
136 }
137
138 async fn label_for_completion(
139 &self,
140 item: &lsp::CompletionItem,
141 language: &Arc<language::Language>,
142 ) -> Option<language::CodeLabel> {
143 use lsp::CompletionItemKind as Kind;
144 let len = item.label.len();
145 let grammar = language.grammar()?;
146 let highlight_id = match item.kind? {
147 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
148 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
149 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
150 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
151 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
152 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
153 _ => None,
154 }?;
155
156 let text = if let Some(description) = item
157 .label_details
158 .as_ref()
159 .and_then(|label_details| label_details.description.as_ref())
160 {
161 format!("{} {}", item.label, description)
162 } else if let Some(detail) = &item.detail {
163 format!("{} {}", item.label, detail)
164 } else {
165 item.label.clone()
166 };
167
168 Some(language::CodeLabel {
169 text,
170 runs: vec![(0..len, highlight_id)],
171 filter_range: 0..len,
172 })
173 }
174
175 async fn initialization_options(
176 self: Arc<Self>,
177 adapter: &Arc<dyn LspAdapterDelegate>,
178 ) -> Result<Option<serde_json::Value>> {
179 let tsdk_path = Self::tsdk_path(&adapter).await;
180 Ok(Some(json!({
181 "typescript": {
182 "tsdk": tsdk_path,
183 "format": {
184 "enable": true
185 },
186 "inlayHints": {
187 "parameterNames": {
188 "enabled": "all",
189 "suppressWhenArgumentMatchesName": false,
190 },
191 "parameterTypes": {
192 "enabled": true
193 },
194 "variableTypes": {
195 "enabled": true,
196 "suppressWhenTypeMatchesName": false,
197 },
198 "propertyDeclarationTypes": {
199 "enabled": true,
200 },
201 "functionLikeReturnTypes": {
202 "enabled": true,
203 },
204 "enumMemberValues": {
205 "enabled": true,
206 }
207 }
208 },
209 "vtsls": {
210 "experimental": {
211 "completion": {
212 "enableServerSideFuzzyMatch": true,
213 "entriesLimit": 5000,
214 }
215 },
216 "autoUseWorkspaceTsdk": true
217 }
218 })))
219 }
220
221 async fn workspace_configuration(
222 self: Arc<Self>,
223 adapter: &Arc<dyn LspAdapterDelegate>,
224 cx: &mut AsyncAppContext,
225 ) -> Result<Value> {
226 let override_options = cx.update(|cx| {
227 ProjectSettings::get_global(cx)
228 .lsp
229 .get(SERVER_NAME)
230 .and_then(|s| s.initialization_options.clone())
231 })?;
232 if let Some(options) = override_options {
233 return Ok(options);
234 }
235 let tsdk_path = Self::tsdk_path(&adapter).await;
236 Ok(json!({
237 "typescript": {
238 "suggest": {
239 "completeFunctionCalls": true
240 },
241 "tsdk": tsdk_path,
242 "format": {
243 "enable": true
244 },
245 "inlayHints": {
246 "parameterNames": {
247 "enabled": "all",
248 "suppressWhenArgumentMatchesName": false,
249 },
250 "parameterTypes": {
251 "enabled": true
252 },
253 "variableTypes": {
254 "enabled": true,
255 "suppressWhenTypeMatchesName": false,
256 },
257 "propertyDeclarationTypes": {
258 "enabled": true,
259 },
260 "functionLikeReturnTypes": {
261 "enabled": true,
262 },
263 "enumMemberValues": {
264 "enabled": true,
265 }
266 }
267 },
268 "vtsls": {
269 "experimental": {
270 "completion": {
271 "enableServerSideFuzzyMatch": true,
272 "entriesLimit": 5000,
273 }
274 },
275 "autoUseWorkspaceTsdk": true
276 }
277 }))
278 }
279
280 fn language_ids(&self) -> HashMap<String, String> {
281 HashMap::from_iter([
282 ("TypeScript".into(), "typescript".into()),
283 ("JavaScript".into(), "javascript".into()),
284 ("TSX".into(), "typescriptreact".into()),
285 ])
286 }
287}
288
289async fn get_cached_ts_server_binary(
290 container_dir: PathBuf,
291 node: &dyn NodeRuntime,
292) -> Option<LanguageServerBinary> {
293 maybe!(async {
294 let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
295 if server_path.exists() {
296 Ok(LanguageServerBinary {
297 path: node.binary_path().await?,
298 env: None,
299 arguments: typescript_server_binary_arguments(&server_path),
300 })
301 } else {
302 Err(anyhow!(
303 "missing executable in directory {:?}",
304 container_dir
305 ))
306 }
307 })
308 .await
309 .log_err()
310}