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