vtsls.rs

  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}