typescript: Add support for VTSLS (#12606)

Piotr Osiewicz created

You can opt into using VTSLS instead of typescript-language-server by
pasting the following snippet into your settings:
```
  "languages": {
    "TSX": {
      "language_servers": [
        "!typescript-language-server",
        "vtsls-language-server"
      ]
    }
  },
```

Related to: #5166 
Release Notes:

- Added support for using [vtsls](https://github.com/yioneko/vtsls)
language server for Typescript/Javascript.

Change summary

crates/languages/src/lib.rs   |  29 ++-
crates/languages/src/vtsls.rs | 237 +++++++++++++++++++++++++++++++++++++
2 files changed, 254 insertions(+), 12 deletions(-)

Detailed changes

crates/languages/src/lib.rs 🔗

@@ -22,6 +22,7 @@ mod python;
 mod rust;
 mod tailwind;
 mod typescript;
+mod vtsls;
 mod yaml;
 
 #[derive(RustEmbed)]
@@ -137,29 +138,33 @@ pub fn init(
     );
     language!(
         "tsx",
-        vec![Arc::new(typescript::TypeScriptLspAdapter::new(
-            node_runtime.clone()
-        ))]
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        ]
     );
     language!(
         "typescript",
-        vec![Arc::new(typescript::TypeScriptLspAdapter::new(
-            node_runtime.clone()
-        ))],
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        ],
         typescript_task_context()
     );
     language!(
         "javascript",
-        vec![Arc::new(typescript::TypeScriptLspAdapter::new(
-            node_runtime.clone()
-        ))],
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
+            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        ],
         typescript_task_context()
     );
     language!(
         "jsdoc",
-        vec![Arc::new(typescript::TypeScriptLspAdapter::new(
-            node_runtime.clone(),
-        ))]
+        vec![
+            Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone(),)),
+            Arc::new(vtsls::VtslsLspAdapter::new(node_runtime.clone()))
+        ]
     );
     language!("regex");
     language!(

crates/languages/src/vtsls.rs 🔗

@@ -0,0 +1,237 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use gpui::AsyncAppContext;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::{CodeActionKind, LanguageServerBinary};
+use node_runtime::NodeRuntime;
+use serde_json::{json, Value};
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{maybe, ResultExt};
+
+fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct VtslsLspAdapter {
+    node: Arc<dyn NodeRuntime>,
+}
+
+impl VtslsLspAdapter {
+    const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
+
+    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
+        VtslsLspAdapter { node }
+    }
+}
+
+struct TypeScriptVersions {
+    typescript_version: String,
+    server_version: String,
+}
+
+#[async_trait(?Send)]
+impl LspAdapter for VtslsLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("vtsls-language-server".into())
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(TypeScriptVersions {
+            typescript_version: self.node.npm_package_latest_version("typescript").await?,
+            server_version: self
+                .node
+                .npm_package_latest_version("@vtsls/language-server")
+                .await?,
+        }) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        latest_version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
+        let server_path = container_dir.join(Self::SERVER_PATH);
+        let package_name = "typescript";
+
+        let should_install_language_server = self
+            .node
+            .should_install_npm_package(
+                package_name,
+                &server_path,
+                &container_dir,
+                latest_version.typescript_version.as_str(),
+            )
+            .await;
+
+        if should_install_language_server {
+            self.node
+                .npm_install_packages(
+                    &container_dir,
+                    &[
+                        (package_name, latest_version.typescript_version.as_str()),
+                        (
+                            "@vtsls/language-server",
+                            latest_version.server_version.as_str(),
+                        ),
+                    ],
+                )
+                .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: self.node.binary_path().await?,
+            env: None,
+            arguments: typescript_server_binary_arguments(&server_path),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &*self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_ts_server_binary(container_dir, &*self.node).await
+    }
+
+    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
+        Some(vec![
+            CodeActionKind::QUICKFIX,
+            CodeActionKind::REFACTOR,
+            CodeActionKind::REFACTOR_EXTRACT,
+            CodeActionKind::SOURCE,
+        ])
+    }
+
+    async fn label_for_completion(
+        &self,
+        item: &lsp::CompletionItem,
+        language: &Arc<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        use lsp::CompletionItemKind as Kind;
+        let len = item.label.len();
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
+            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
+            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
+            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
+            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
+            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
+            _ => None,
+        }?;
+
+        let text = match &item.detail {
+            Some(detail) => format!("{} {}", item.label, detail),
+            None => item.label.clone(),
+        };
+
+        Some(language::CodeLabel {
+            text,
+            runs: vec![(0..len, highlight_id)],
+            filter_range: 0..len,
+        })
+    }
+
+    async fn initialization_options(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<serde_json::Value>> {
+        Ok(Some(json!({
+            "typescript":
+            {
+                "tsdk": "node_modules/typescript/lib",
+                "format": {
+                    "enable": true
+                },
+                "inlayHints":{
+                    "parameterNames":
+                    {
+                        "enabled": "all",
+                        "suppressWhenArgumentMatchesName": false,
+
+                    }
+                    },
+                    "parameterTypes":
+                    {
+                        "enabled": true
+                    },
+                    "variableTypes": {
+                        "enabled": true,
+                        "suppressWhenTypeMatchesName": false,
+                    },
+                    "propertyDeclarationTypes":{
+                        "enabled": true,
+                    },
+                    "functionLikeReturnTypes": {
+                        "enabled": true,
+                    },
+                    "enumMemberValues":{
+                        "enabled": true,
+                    }
+            }
+        })))
+    }
+
+    async fn workspace_configuration(
+        self: Arc<Self>,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _cx: &mut AsyncAppContext,
+    ) -> Result<Value> {
+        Ok(json!({
+            "typescript": {
+                "suggest": {
+                    "completeFunctionCalls": true
+                }
+            }
+        }))
+    }
+
+    fn language_ids(&self) -> HashMap<String, String> {
+        HashMap::from_iter([
+            ("TypeScript".into(), "typescript".into()),
+            ("JavaScript".into(), "javascript".into()),
+            ("TSX".into(), "typescriptreact".into()),
+        ])
+    }
+}
+
+async fn get_cached_ts_server_binary(
+    container_dir: PathBuf,
+    node: &dyn NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    maybe!(async {
+        let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
+        if server_path.exists() {
+            Ok(LanguageServerBinary {
+                path: node.binary_path().await?,
+                env: None,
+                arguments: typescript_server_binary_arguments(&server_path),
+            })
+        } else {
+            Err(anyhow!(
+                "missing executable in directory {:?}",
+                container_dir
+            ))
+        }
+    })
+    .await
+    .log_err()
+}