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 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 _: Option<Uri>,
232 cx: &mut AsyncApp,
233 ) -> Result<Value> {
234 let tsdk_path = self.tsdk_path(delegate).await;
235 let config = serde_json::json!({
236 "tsdk": tsdk_path,
237 "suggest": {
238 "completeFunctionCalls": true
239 },
240 "inlayHints": {
241 "parameterNames": {
242 "enabled": "all",
243 "suppressWhenArgumentMatchesName": false
244 },
245 "parameterTypes": {
246 "enabled": true
247 },
248 "variableTypes": {
249 "enabled": true,
250 "suppressWhenTypeMatchesName": false
251 },
252 "propertyDeclarationTypes": {
253 "enabled": true
254 },
255 "functionLikeReturnTypes": {
256 "enabled": true
257 },
258 "enumMemberValues": {
259 "enabled": true
260 }
261 },
262 "tsserver": {
263 "maxTsServerMemory": 8092
264 },
265 });
266
267 let mut default_workspace_configuration = serde_json::json!({
268 "typescript": config,
269 "javascript": config,
270 "vtsls": {
271 "experimental": {
272 "completion": {
273 "enableServerSideFuzzyMatch": true,
274 "entriesLimit": 5000,
275 }
276 },
277 "autoUseWorkspaceTsdk": true
278 }
279 });
280
281 let override_options = cx.update(|cx| {
282 language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
283 .and_then(|s| s.settings.clone())
284 })?;
285
286 if let Some(override_options) = override_options {
287 merge_json_value_into(override_options, &mut default_workspace_configuration)
288 }
289
290 Ok(default_workspace_configuration)
291 }
292
293 fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
294 VtslsLspAdapter::enhance_diagnostic_message(message)
295 }
296
297 fn language_ids(&self) -> HashMap<LanguageName, String> {
298 HashMap::from_iter([
299 (LanguageName::new("TypeScript"), "typescript".into()),
300 (LanguageName::new("JavaScript"), "javascript".into()),
301 (LanguageName::new("TSX"), "typescriptreact".into()),
302 ])
303 }
304}
305
306async fn get_cached_ts_server_binary(
307 container_dir: PathBuf,
308 node: &NodeRuntime,
309) -> Option<LanguageServerBinary> {
310 maybe!(async {
311 let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
312 anyhow::ensure!(
313 server_path.exists(),
314 "missing executable in directory {container_dir:?}"
315 );
316 Ok(LanguageServerBinary {
317 path: node.binary_path().await?,
318 env: None,
319 arguments: typescript_server_binary_arguments(&server_path),
320 })
321 })
322 .await
323 .log_err()
324}
325
326#[cfg(test)]
327mod tests {
328 use crate::vtsls::VtslsLspAdapter;
329
330 #[test]
331 fn test_diagnostic_message_to_markdown() {
332 // Leaves simple messages unchanged
333 let message = "The expected type comes from the return type of this signature.";
334
335 let expected = "The expected type comes from the return type of this signature.";
336
337 assert_eq!(
338 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
339 expected
340 );
341
342 // Parses both multi-word and single-word correctly
343 let message = "Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
344
345 let expected = "Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
346
347 assert_eq!(
348 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
349 expected
350 );
351
352 // Parses multi-and-single word in any order, and ignores existing newlines
353 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'.";
354
355 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`.";
356
357 assert_eq!(
358 VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
359 expected
360 );
361 }
362}