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 "tsserver": {
273 "maxTsServerMemory": 8092
274 },
275 });
276
277 let mut default_workspace_configuration = serde_json::json!({
278 "typescript": config,
279 "javascript": config,
280 "vtsls": {
281 "experimental": {
282 "completion": {
283 "enableServerSideFuzzyMatch": true,
284 "entriesLimit": 5000,
285 }
286 },
287 "autoUseWorkspaceTsdk": true
288 }
289 });
290
291 let override_options = cx.update(|cx| {
292 language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
293 .and_then(|s| s.settings.clone())
294 })?;
295
296 if let Some(override_options) = override_options {
297 merge_json_value_into(override_options, &mut default_workspace_configuration)
298 }
299
300 Ok(default_workspace_configuration)
301 }
302
303 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
304 VtslsLspAdapter::enhance_diagnostic_message(message)
305 }
306
307 fn language_ids(&self) -> HashMap<LanguageName, String> {
308 HashMap::from_iter([
309 (LanguageName::new_static("TypeScript"), "typescript".into()),
310 (LanguageName::new_static("JavaScript"), "javascript".into()),
311 (LanguageName::new_static("TSX"), "typescriptreact".into()),
312 ])
313 }
314
315 fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
316 let selected_title = context.selected_action.title.as_str();
317 let is_preference_response =
318 selected_title == ACTION_ALWAYS || selected_title == ACTION_NEVER;
319 if !is_preference_response {
320 return;
321 }
322
323 if context.message.contains(UPDATE_IMPORTS_MESSAGE_PATTERN) {
324 let setting_value = match selected_title {
325 ACTION_ALWAYS => "always",
326 ACTION_NEVER => "never",
327 _ => return,
328 };
329
330 let settings = json!({
331 "typescript": {
332 "updateImportsOnFileMove": {
333 "enabled": setting_value
334 }
335 },
336 "javascript": {
337 "updateImportsOnFileMove": {
338 "enabled": setting_value
339 }
340 }
341 });
342
343 let _ = cx.update(|cx| {
344 update_settings_file(self.fs.clone(), cx, move |content, _| {
345 let lsp_settings = content
346 .project
347 .lsp
348 .0
349 .entry(VTSLS_SERVER_NAME.into())
350 .or_default();
351
352 if let Some(existing) = &mut lsp_settings.settings {
353 merge_json_value_into(settings, existing);
354 } else {
355 lsp_settings.settings = Some(settings);
356 }
357 });
358 });
359 }
360 }
361}
362
363async fn get_cached_ts_server_binary(
364 container_dir: PathBuf,
365 node: &NodeRuntime,
366) -> Option<LanguageServerBinary> {
367 maybe!(async {
368 let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
369 anyhow::ensure!(
370 server_path.exists(),
371 "missing executable in directory {container_dir:?}"
372 );
373 Ok(LanguageServerBinary {
374 path: node.binary_path().await?,
375 env: None,
376 arguments: typescript_server_binary_arguments(&server_path),
377 })
378 })
379 .await
380 .log_err()
381}
382
383#[cfg(test)]
384mod tests {
385 use crate::vtsls::VtslsLspAdapter;
386
387 #[test]
388 fn test_diagnostic_message_to_markdown() {
389 // Leaves simple messages unchanged
390 let message = "The expected type comes from the return type of this signature.";
391
392 let expected = "The expected type comes from the return type of this signature.";
393
394 assert_eq!(
395 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
396 expected
397 );
398
399 // Parses both multi-word and single-word correctly
400 let message = "Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
401
402 let expected = "Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
403
404 assert_eq!(
405 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
406 expected
407 );
408
409 // Parses multi-and-single word in any order, and ignores existing newlines
410 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'.";
411
412 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`.";
413
414 assert_eq!(
415 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
416 expected
417 );
418 }
419}