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