1use anyhow::Result;
2use async_trait::async_trait;
3use collections::HashMap;
4use gpui::AsyncApp;
5use language::{
6 LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, PromptResponseContext, Toolchain,
7};
8use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
9use node_runtime::{NodeRuntime, VersionStrategy};
10use project::{Fs, lsp_store::language_server_settings};
11use regex::Regex;
12use semver::Version;
13use serde_json::Value;
14use serde_json::json;
15use settings::update_settings_file;
16use std::{
17 ffi::OsString,
18 path::{Path, PathBuf},
19 sync::{Arc, LazyLock},
20};
21use util::{ResultExt, maybe, merge_json_value_into};
22
23const ACTION_ALWAYS: &str = "Always";
24const ACTION_NEVER: &str = "Never";
25const UPDATE_IMPORTS_MESSAGE_PATTERN: &str = "Update imports for";
26const VTSLS_SERVER_NAME: &str = "vtsls";
27
28fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
29 vec![server_path.into(), "--stdio".into()]
30}
31
32pub struct VtslsLspAdapter {
33 node: NodeRuntime,
34 fs: Arc<dyn Fs>,
35}
36
37impl VtslsLspAdapter {
38 const PACKAGE_NAME: &'static str = "@vtsls/language-server";
39 const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
40
41 const TYPESCRIPT_PACKAGE_NAME: &'static str = "typescript";
42 const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib";
43 const TYPESCRIPT_YARN_TSDK_PATH: &'static str = ".yarn/sdks/typescript/lib";
44
45 pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
46 VtslsLspAdapter { node, fs }
47 }
48
49 async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
50 let yarn_sdk = adapter
51 .worktree_root_path()
52 .join(Self::TYPESCRIPT_YARN_TSDK_PATH);
53
54 let tsdk_path = if self.fs.is_dir(&yarn_sdk).await {
55 Self::TYPESCRIPT_YARN_TSDK_PATH
56 } else {
57 Self::TYPESCRIPT_TSDK_PATH
58 };
59
60 if self
61 .fs
62 .is_dir(&adapter.worktree_root_path().join(tsdk_path))
63 .await
64 {
65 Some(tsdk_path)
66 } else {
67 None
68 }
69 }
70
71 pub fn enhance_diagnostic_message(message: &str) -> Option<String> {
72 static SINGLE_WORD_REGEX: LazyLock<Regex> =
73 LazyLock::new(|| Regex::new(r"'([^\s']*)'").expect("Failed to create REGEX"));
74
75 static MULTI_WORD_REGEX: LazyLock<Regex> =
76 LazyLock::new(|| Regex::new(r"'([^']+\s+[^']*)'").expect("Failed to create REGEX"));
77
78 let first = SINGLE_WORD_REGEX.replace_all(message, "`$1`").to_string();
79 let second = MULTI_WORD_REGEX
80 .replace_all(&first, "\n```typescript\n$1\n```\n")
81 .to_string();
82 Some(second)
83 }
84}
85
86pub struct TypeScriptVersions {
87 typescript_version: Version,
88 server_version: Version,
89}
90
91const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
92
93impl LspInstaller for VtslsLspAdapter {
94 type BinaryVersion = TypeScriptVersions;
95
96 async fn fetch_latest_server_version(
97 &self,
98 _: &dyn LspAdapterDelegate,
99 _: bool,
100 _: &mut AsyncApp,
101 ) -> Result<Self::BinaryVersion> {
102 Ok(TypeScriptVersions {
103 typescript_version: self.node.npm_package_latest_version("typescript").await?,
104 server_version: self
105 .node
106 .npm_package_latest_version("@vtsls/language-server")
107 .await?,
108 })
109 }
110
111 async fn check_if_user_installed(
112 &self,
113 delegate: &dyn LspAdapterDelegate,
114 _: Option<Toolchain>,
115 _: &AsyncApp,
116 ) -> Option<LanguageServerBinary> {
117 let env = delegate.shell_env().await;
118 let path = delegate.which(SERVER_NAME.as_ref()).await?;
119 Some(LanguageServerBinary {
120 path: path.clone(),
121 arguments: typescript_server_binary_arguments(&path),
122 env: Some(env),
123 })
124 }
125
126 async fn fetch_server_binary(
127 &self,
128 latest_version: Self::BinaryVersion,
129 container_dir: PathBuf,
130 _: &dyn LspAdapterDelegate,
131 ) -> Result<LanguageServerBinary> {
132 let server_path = container_dir.join(Self::SERVER_PATH);
133
134 let typescript_version = latest_version.typescript_version.to_string();
135 let server_version = latest_version.server_version.to_string();
136
137 let mut packages_to_install = Vec::new();
138
139 if self
140 .node
141 .should_install_npm_package(
142 Self::PACKAGE_NAME,
143 &server_path,
144 &container_dir,
145 VersionStrategy::Latest(&latest_version.server_version),
146 )
147 .await
148 {
149 packages_to_install.push((Self::PACKAGE_NAME, server_version.as_str()));
150 }
151
152 if self
153 .node
154 .should_install_npm_package(
155 Self::TYPESCRIPT_PACKAGE_NAME,
156 &container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
157 &container_dir,
158 VersionStrategy::Latest(&latest_version.typescript_version),
159 )
160 .await
161 {
162 packages_to_install.push((Self::TYPESCRIPT_PACKAGE_NAME, typescript_version.as_str()));
163 }
164
165 self.node
166 .npm_install_packages(&container_dir, &packages_to_install)
167 .await?;
168
169 Ok(LanguageServerBinary {
170 path: self.node.binary_path().await?,
171 env: None,
172 arguments: typescript_server_binary_arguments(&server_path),
173 })
174 }
175
176 async fn cached_server_binary(
177 &self,
178 container_dir: PathBuf,
179 _: &dyn LspAdapterDelegate,
180 ) -> Option<LanguageServerBinary> {
181 get_cached_ts_server_binary(container_dir, &self.node).await
182 }
183}
184
185#[async_trait(?Send)]
186impl LspAdapter for VtslsLspAdapter {
187 fn name(&self) -> LanguageServerName {
188 SERVER_NAME
189 }
190
191 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
192 Some(vec![
193 CodeActionKind::QUICKFIX,
194 CodeActionKind::REFACTOR,
195 CodeActionKind::REFACTOR_EXTRACT,
196 CodeActionKind::SOURCE,
197 ])
198 }
199
200 async fn label_for_completion(
201 &self,
202 item: &lsp::CompletionItem,
203 language: &Arc<language::Language>,
204 ) -> Option<language::CodeLabel> {
205 use lsp::CompletionItemKind as Kind;
206 let label_len = item.label.len();
207 let grammar = language.grammar()?;
208 let highlight_id = match item.kind? {
209 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
210 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
211 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
212 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
213 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
214 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
215 _ => None,
216 }?;
217
218 let text = if let Some(description) = item
219 .label_details
220 .as_ref()
221 .and_then(|label_details| label_details.description.as_ref())
222 {
223 format!("{} {}", item.label, description)
224 } else if let Some(detail) = &item.detail {
225 format!("{} {}", item.label, detail)
226 } else {
227 item.label.clone()
228 };
229 Some(language::CodeLabel::filtered(
230 text,
231 label_len,
232 item.filter_text.as_deref(),
233 vec![(0..label_len, highlight_id)],
234 ))
235 }
236
237 async fn workspace_configuration(
238 self: Arc<Self>,
239 delegate: &Arc<dyn LspAdapterDelegate>,
240 _: Option<Toolchain>,
241 _: Option<Uri>,
242 cx: &mut AsyncApp,
243 ) -> Result<Value> {
244 let tsdk_path = self.tsdk_path(delegate).await;
245 let config = serde_json::json!({
246 "tsdk": tsdk_path,
247 "suggest": {
248 "completeFunctionCalls": true
249 },
250 "inlayHints": {
251 "parameterNames": {
252 "enabled": "all",
253 "suppressWhenArgumentMatchesName": false
254 },
255 "parameterTypes": {
256 "enabled": true
257 },
258 "variableTypes": {
259 "enabled": true,
260 "suppressWhenTypeMatchesName": false
261 },
262 "propertyDeclarationTypes": {
263 "enabled": true
264 },
265 "functionLikeReturnTypes": {
266 "enabled": true
267 },
268 "enumMemberValues": {
269 "enabled": true
270 }
271 },
272 "implementationsCodeLens": {
273 "enabled": true,
274 "showOnAllClassMethods": true,
275 "showOnInterfaceMethods": true
276 },
277 "referencesCodeLens": {
278 "enabled": true,
279 "showOnAllFunctions": true
280 },
281 "tsserver": {
282 "maxTsServerMemory": 8092
283 },
284 });
285
286 let mut default_workspace_configuration = serde_json::json!({
287 "typescript": config,
288 "javascript": config,
289 "vtsls": {
290 "experimental": {
291 "completion": {
292 "enableServerSideFuzzyMatch": true,
293 "entriesLimit": 5000,
294 }
295 },
296 "autoUseWorkspaceTsdk": true
297 }
298 });
299
300 let override_options = cx.update(|cx| {
301 language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
302 .and_then(|s| s.settings.clone())
303 });
304
305 if let Some(override_options) = override_options {
306 merge_json_value_into(override_options, &mut default_workspace_configuration)
307 }
308
309 Ok(default_workspace_configuration)
310 }
311
312 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
313 VtslsLspAdapter::enhance_diagnostic_message(message)
314 }
315
316 fn language_ids(&self) -> HashMap<LanguageName, String> {
317 HashMap::from_iter([
318 (LanguageName::new_static("TypeScript"), "typescript".into()),
319 (LanguageName::new_static("JavaScript"), "javascript".into()),
320 (LanguageName::new_static("TSX"), "typescriptreact".into()),
321 ])
322 }
323
324 fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
325 let selected_title = context.selected_action.title.as_str();
326 let is_preference_response =
327 selected_title == ACTION_ALWAYS || selected_title == ACTION_NEVER;
328 if !is_preference_response {
329 return;
330 }
331
332 if context.message.contains(UPDATE_IMPORTS_MESSAGE_PATTERN) {
333 let setting_value = match selected_title {
334 ACTION_ALWAYS => "always",
335 ACTION_NEVER => "never",
336 _ => return,
337 };
338
339 let settings = json!({
340 "typescript": {
341 "updateImportsOnFileMove": {
342 "enabled": setting_value
343 }
344 },
345 "javascript": {
346 "updateImportsOnFileMove": {
347 "enabled": setting_value
348 }
349 }
350 });
351
352 let _ = cx.update(|cx| {
353 update_settings_file(self.fs.clone(), cx, move |content, _| {
354 let lsp_settings = content
355 .project
356 .lsp
357 .0
358 .entry(VTSLS_SERVER_NAME.into())
359 .or_default();
360
361 if let Some(existing) = &mut lsp_settings.settings {
362 merge_json_value_into(settings, existing);
363 } else {
364 lsp_settings.settings = Some(settings);
365 }
366 });
367 });
368 }
369 }
370}
371
372async fn get_cached_ts_server_binary(
373 container_dir: PathBuf,
374 node: &NodeRuntime,
375) -> Option<LanguageServerBinary> {
376 maybe!(async {
377 let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
378 anyhow::ensure!(
379 server_path.exists(),
380 "missing executable in directory {container_dir:?}"
381 );
382 Ok(LanguageServerBinary {
383 path: node.binary_path().await?,
384 env: None,
385 arguments: typescript_server_binary_arguments(&server_path),
386 })
387 })
388 .await
389 .log_err()
390}
391
392#[cfg(test)]
393mod tests {
394 use crate::vtsls::VtslsLspAdapter;
395
396 #[test]
397 fn test_diagnostic_message_to_markdown() {
398 // Leaves simple messages unchanged
399 let message = "The expected type comes from the return type of this signature.";
400
401 let expected = "The expected type comes from the return type of this signature.";
402
403 assert_eq!(
404 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
405 expected
406 );
407
408 // Parses both multi-word and single-word correctly
409 let message = "Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
410
411 let expected = "Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
412
413 assert_eq!(
414 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
415 expected
416 );
417
418 // Parses multi-and-single word in any order, and ignores existing newlines
419 let message = "Type '() => { foo: string; bar: string; }' is not assignable to type 'GetUserFunction'.\n Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
420
421 let expected = "Type \n```typescript\n() => { foo: string; bar: string; }\n```\n is not assignable to type `GetUserFunction`.\n Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
422
423 assert_eq!(
424 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
425 expected
426 );
427 }
428}