vtsls.rs

  1use anyhow::Result;
  2use async_trait::async_trait;
  3use collections::HashMap;
  4use gpui::AsyncApp;
  5use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
  6use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
  7use node_runtime::NodeRuntime;
  8use project::{Fs, lsp_store::language_server_settings};
  9use serde_json::Value;
 10use std::{
 11    any::Any,
 12    ffi::OsString,
 13    path::{Path, PathBuf},
 14    sync::Arc,
 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}
 25
 26impl VtslsLspAdapter {
 27    const PACKAGE_NAME: &'static str = "@vtsls/language-server";
 28    const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
 29
 30    const TYPESCRIPT_PACKAGE_NAME: &'static str = "typescript";
 31    const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib";
 32
 33    pub fn new(node: NodeRuntime) -> Self {
 34        VtslsLspAdapter { node }
 35    }
 36
 37    async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
 38        let is_yarn = adapter
 39            .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
 40            .await
 41            .is_ok();
 42
 43        let tsdk_path = if is_yarn {
 44            ".yarn/sdks/typescript/lib"
 45        } else {
 46            Self::TYPESCRIPT_TSDK_PATH
 47        };
 48
 49        if fs
 50            .is_dir(&adapter.worktree_root_path().join(tsdk_path))
 51            .await
 52        {
 53            Some(tsdk_path)
 54        } else {
 55            None
 56        }
 57    }
 58}
 59
 60struct TypeScriptVersions {
 61    typescript_version: String,
 62    server_version: String,
 63}
 64
 65const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
 66
 67#[async_trait(?Send)]
 68impl LspAdapter for VtslsLspAdapter {
 69    fn name(&self) -> LanguageServerName {
 70        SERVER_NAME.clone()
 71    }
 72
 73    async fn fetch_latest_server_version(
 74        &self,
 75        _: &dyn LspAdapterDelegate,
 76    ) -> Result<Box<dyn 'static + Send + Any>> {
 77        Ok(Box::new(TypeScriptVersions {
 78            typescript_version: self.node.npm_package_latest_version("typescript").await?,
 79            server_version: self
 80                .node
 81                .npm_package_latest_version("@vtsls/language-server")
 82                .await?,
 83        }) as Box<_>)
 84    }
 85
 86    async fn check_if_user_installed(
 87        &self,
 88        delegate: &dyn LspAdapterDelegate,
 89        _: Arc<dyn LanguageToolchainStore>,
 90        _: &AsyncApp,
 91    ) -> Option<LanguageServerBinary> {
 92        let env = delegate.shell_env().await;
 93        let path = delegate.which(SERVER_NAME.as_ref()).await?;
 94        Some(LanguageServerBinary {
 95            path: path.clone(),
 96            arguments: typescript_server_binary_arguments(&path),
 97            env: Some(env),
 98        })
 99    }
100
101    async fn fetch_server_binary(
102        &self,
103        latest_version: Box<dyn 'static + Send + Any>,
104        container_dir: PathBuf,
105        _: &dyn LspAdapterDelegate,
106    ) -> Result<LanguageServerBinary> {
107        let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
108        let server_path = container_dir.join(Self::SERVER_PATH);
109
110        let mut packages_to_install = Vec::new();
111
112        if self
113            .node
114            .should_install_npm_package(
115                Self::PACKAGE_NAME,
116                &server_path,
117                &container_dir,
118                &latest_version.server_version,
119            )
120            .await
121        {
122            packages_to_install.push((Self::PACKAGE_NAME, latest_version.server_version.as_str()));
123        }
124
125        if self
126            .node
127            .should_install_npm_package(
128                Self::TYPESCRIPT_PACKAGE_NAME,
129                &container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
130                &container_dir,
131                &latest_version.typescript_version,
132            )
133            .await
134        {
135            packages_to_install.push((
136                Self::TYPESCRIPT_PACKAGE_NAME,
137                latest_version.typescript_version.as_str(),
138            ));
139        }
140
141        self.node
142            .npm_install_packages(&container_dir, &packages_to_install)
143            .await?;
144
145        Ok(LanguageServerBinary {
146            path: self.node.binary_path().await?,
147            env: None,
148            arguments: typescript_server_binary_arguments(&server_path),
149        })
150    }
151
152    async fn cached_server_binary(
153        &self,
154        container_dir: PathBuf,
155        _: &dyn LspAdapterDelegate,
156    ) -> Option<LanguageServerBinary> {
157        get_cached_ts_server_binary(container_dir, &self.node).await
158    }
159
160    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
161        Some(vec![
162            CodeActionKind::QUICKFIX,
163            CodeActionKind::REFACTOR,
164            CodeActionKind::REFACTOR_EXTRACT,
165            CodeActionKind::SOURCE,
166        ])
167    }
168
169    async fn label_for_completion(
170        &self,
171        item: &lsp::CompletionItem,
172        language: &Arc<language::Language>,
173    ) -> Option<language::CodeLabel> {
174        use lsp::CompletionItemKind as Kind;
175        let len = item.label.len();
176        let grammar = language.grammar()?;
177        let highlight_id = match item.kind? {
178            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
179            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
180            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
181            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
182            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
183            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
184            _ => None,
185        }?;
186
187        let text = if let Some(description) = item
188            .label_details
189            .as_ref()
190            .and_then(|label_details| label_details.description.as_ref())
191        {
192            format!("{} {}", item.label, description)
193        } else if let Some(detail) = &item.detail {
194            format!("{} {}", item.label, detail)
195        } else {
196            item.label.clone()
197        };
198        let filter_range = item
199            .filter_text
200            .as_deref()
201            .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
202            .unwrap_or(0..len);
203        Some(language::CodeLabel {
204            text,
205            runs: vec![(0..len, highlight_id)],
206            filter_range,
207        })
208    }
209
210    async fn workspace_configuration(
211        self: Arc<Self>,
212        fs: &dyn Fs,
213        delegate: &Arc<dyn LspAdapterDelegate>,
214        _: Arc<dyn LanguageToolchainStore>,
215        cx: &mut AsyncApp,
216    ) -> Result<Value> {
217        let tsdk_path = Self::tsdk_path(fs, delegate).await;
218        let config = serde_json::json!({
219            "tsdk": tsdk_path,
220            "suggest": {
221                "completeFunctionCalls": true
222            },
223            "inlayHints": {
224                "parameterNames": {
225                    "enabled": "all",
226                    "suppressWhenArgumentMatchesName": false
227                },
228                "parameterTypes": {
229                    "enabled": true
230                },
231                "variableTypes": {
232                    "enabled": true,
233                    "suppressWhenTypeMatchesName": false
234                },
235                "propertyDeclarationTypes": {
236                    "enabled": true
237                },
238                "functionLikeReturnTypes": {
239                    "enabled": true
240                },
241                "enumMemberValues": {
242                    "enabled": true
243                }
244            },
245            "tsserver": {
246                "maxTsServerMemory": 8092
247            },
248        });
249
250        let mut default_workspace_configuration = serde_json::json!({
251            "typescript": config,
252            "javascript": config,
253            "vtsls": {
254                "experimental": {
255                    "completion": {
256                        "enableServerSideFuzzyMatch": true,
257                        "entriesLimit": 5000,
258                    }
259                },
260               "autoUseWorkspaceTsdk": true
261            }
262        });
263
264        let override_options = cx.update(|cx| {
265            language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
266                .and_then(|s| s.settings.clone())
267        })?;
268
269        if let Some(override_options) = override_options {
270            merge_json_value_into(override_options, &mut default_workspace_configuration)
271        }
272
273        Ok(default_workspace_configuration)
274    }
275
276    fn language_ids(&self) -> HashMap<String, String> {
277        HashMap::from_iter([
278            ("TypeScript".into(), "typescript".into()),
279            ("JavaScript".into(), "javascript".into()),
280            ("TSX".into(), "typescriptreact".into()),
281        ])
282    }
283
284    fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
285        use regex::{Captures, Regex};
286        dbg!(&message);
287
288        // Helper functions for formatting
289        let format_type_block = |prefix: &str, content: &str| -> String {
290            if prefix.is_empty() {
291                if content.len() > 50 || content.contains('\n') || content.contains('`') {
292                    format!("\n```typescript\ntype a ={}\n```\n", dbg!(content))
293                } else {
294                    format!("`{}`", dbg!(content))
295                }
296            } else {
297                if content.len() > 50 || content.contains('\n') || content.contains('`') {
298                    format!(
299                        "{}\n```typescript\ntype a ={}\n```\n",
300                        prefix,
301                        dbg!(content)
302                    )
303                } else {
304                    format!("{} `{}`", prefix, dbg!(content))
305                }
306            }
307        };
308
309        let format_typescript_block =
310            |content: &str| -> String { format!("\n\n```typescript\n{}\n```\n", dbg!(content)) };
311
312        let format_simple_type_block = |content: &str| -> String { format!("`{}`", dbg!(content)) };
313
314        let unstyle_code_block = |content: &str| -> String { format!("`{}`", dbg!(content)) };
315
316        let mut result = message.to_string();
317
318        // Format 'key' with "value"
319        let re = Regex::new(r#"(\w+)(\s+)'(.+?)'(\s+)with(\s+)"(.+?)""#).unwrap();
320        result = re
321            .replace_all(&result, |caps: &Captures| {
322                format!(
323                    "{}{}`{}`{} with `\"{}\"`",
324                    &caps[1], &caps[2], &caps[3], &caps[4], &caps[6]
325                )
326            })
327            .to_string();
328
329        // Format "key"
330        let re = Regex::new(r#"(\s)'"(.*?)"'(\s|:|.|$)"#).unwrap();
331        result = re
332            .replace_all(&result, |caps: &Captures| {
333                format!("{}`\"{}\"`{}", &caps[1], &caps[2], &caps[3])
334            })
335            .to_string();
336
337        // Format declare module snippet
338        let re = Regex::new(r#"['"](declare module )['"](.*)['""];['"']"#).unwrap();
339        result = re
340            .replace_all(&result, |caps: &Captures| {
341                format_typescript_block(&format!("{} \"{}\"", &caps[1], &caps[2]))
342            })
343            .to_string();
344
345        // Format missing props error
346        let re = Regex::new(r#"(is missing the following properties from type\s?)'(.*)': ([^:]+)"#)
347            .unwrap();
348        result = re
349            .replace_all(&result, |caps: &Captures| {
350                let props: Vec<&str> = caps[3].split(", ").filter(|s| !s.is_empty()).collect();
351                let props_html = props
352                    .iter()
353                    .map(|prop| format!("<li>{}</li>", prop))
354                    .collect::<Vec<_>>()
355                    .join("");
356                format!("{}`{}`: <ul>{}</ul>", &caps[1], &caps[2], props_html)
357            })
358            .to_string();
359
360        // Format type pairs
361        let re = Regex::new(r#"(?i)(types) ['"](.*?)['"] and ['"](.*?)['"][.]?"#).unwrap();
362        result = re
363            .replace_all(&result, |caps: &Captures| {
364                format!("{} `{}` and `{}`", &caps[1], &caps[2], &caps[3])
365            })
366            .to_string();
367
368        // Format type annotation options
369        let re = Regex::new(r#"(?i)type annotation must be ['"](.*?)['"] or ['"](.*?)['"][.]?"#)
370            .unwrap();
371        result = re
372            .replace_all(&result, |caps: &Captures| {
373                format!("type annotation must be `{}` or `{}`", &caps[1], &caps[2])
374            })
375            .to_string();
376
377        // Format overload
378        let re = Regex::new(r#"(?i)(Overload \d of \d), ['"](.*?)['"], "#).unwrap();
379        result = re
380            .replace_all(&result, |caps: &Captures| {
381                format!("{}, `{}`, ", &caps[1], &caps[2])
382            })
383            .to_string();
384
385        // Format simple strings
386        let re = Regex::new(r#"^['"]"[^"]*"['"]$"#).unwrap();
387        result = re
388            .replace_all(&result, |caps: &Captures| format_typescript_block(&caps[0]))
389            .to_string();
390
391        // Replace module 'x' by module "x" for ts error #2307
392        let re = Regex::new(r#"(?i)(module )'([^"]*?)'"#).unwrap();
393        result = re
394            .replace_all(&result, |caps: &Captures| {
395                format!("{}\"{}\"", &caps[1], &caps[2])
396            })
397            .to_string();
398
399        // Format string types
400        let re = Regex::new(r#"(?i)(module|file|file name|imported via) ['""](.*?)['""]"#).unwrap();
401        result = re
402            .replace_all(&result, |caps: &Captures| {
403                format_type_block(&caps[1], &format!("\"{}\"", &caps[2]))
404            })
405            .to_string();
406
407        // Format types
408        dbg!(&result);
409        let re = Regex::new(r#"(?i)(type|type alias|interface|module|file|file name|class|method's|subtype of constraint) ['"](.*?)['"]"#).unwrap();
410        result = re
411            .replace_all(&result, |caps: &Captures| {
412                dbg!(&caps);
413                format_type_block(&caps[1], &caps[2])
414            })
415            .to_string();
416
417        // Format reversed types
418        let re = Regex::new(r#"(?i)(.*)['"]([^>]*)['"] (type|interface|return type|file|module|is (not )?assignable)"#).unwrap();
419        result = re
420            .replace_all(&result, |caps: &Captures| {
421                format!("{}`{}` {}", &caps[1], &caps[2], &caps[3])
422            })
423            .to_string();
424
425        // Format simple types that didn't captured before
426        let re = Regex::new(
427            r#"['"]((void|null|undefined|any|boolean|string|number|bigint|symbol)(\[\])?)['"']"#,
428        )
429        .unwrap();
430        result = re
431            .replace_all(&result, |caps: &Captures| {
432                format_simple_type_block(&caps[1])
433            })
434            .to_string();
435
436        // Format some typescript keywords
437        let re = Regex::new(r#"['"](import|export|require|in|continue|break|let|false|true|const|new|throw|await|for await|[0-9]+)( ?.*?)['"]"#).unwrap();
438        result = re
439            .replace_all(&result, |caps: &Captures| {
440                format_typescript_block(&format!("{}{}", &caps[1], &caps[2]))
441            })
442            .to_string();
443
444        // Format return values
445        let re = Regex::new(r#"(?i)(return|operator) ['"](.*?)['"']"#).unwrap();
446        result = re
447            .replace_all(&result, |caps: &Captures| {
448                format!("{} {}", &caps[1], format_typescript_block(&caps[2]))
449            })
450            .to_string();
451
452        // Format regular code blocks
453        let re = Regex::new(r#"(\W|^)'([^'"]*?)'(\W|$)"#).unwrap();
454        result = re
455            .replace_all(&result, |caps: &Captures| {
456                format!("{}{}{}", &caps[1], unstyle_code_block(&caps[2]), &caps[3])
457            })
458            .to_string();
459
460        Some(result)
461    }
462}
463
464async fn get_cached_ts_server_binary(
465    container_dir: PathBuf,
466    node: &NodeRuntime,
467) -> Option<LanguageServerBinary> {
468    maybe!(async {
469        let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
470        anyhow::ensure!(
471            server_path.exists(),
472            "missing executable in directory {container_dir:?}"
473        );
474        Ok(LanguageServerBinary {
475            path: node.binary_path().await?,
476            env: None,
477            arguments: typescript_server_binary_arguments(&server_path),
478        })
479    })
480    .await
481    .log_err()
482}
483
484#[cfg(test)]
485mod test {
486    use super::*;
487    use indoc::indoc;
488
489    #[test]
490    fn test_diagnostic_message_to_markdown() {
491        let message = "Property 'user' is missing in type '{ person: { username: string; email: string; }; }' but required in type '{ user: { name: string; email: `${string}@${string}.${string}`; age: number; }; }'.";
492        let expected = indoc! { "
493            Property `user` is missing in type `{ person: { username: string; email: string; }; }` but required in type
494
495            ```typescript
496            { user: { name: string; email: `${string}@${string}.${string}`; age: number; }; }
497            ```
498        "};
499        let result = VtslsLspAdapter::new(NodeRuntime::unavailable())
500            .diagnostic_message_to_markdown(message)
501            .unwrap();
502        pretty_assertions::assert_eq!(result, expected.to_string());
503    }
504}