diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index b9ee0f1804..f2860f2495 100644 --- a/crates/languages/src/lib.rs +++ b/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!( diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs new file mode 100644 index 0000000000..37002c2001 --- /dev/null +++ b/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 { + vec![server_path.into(), "--stdio".into()] +} + +pub struct VtslsLspAdapter { + node: Arc, +} + +impl VtslsLspAdapter { + const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js"; + + pub fn new(node: Arc) -> 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> { + 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, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let latest_version = latest_version.downcast::().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 { + get_cached_ts_server_binary(container_dir, &*self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_ts_server_binary(container_dir, &*self.node).await + } + + fn code_action_kinds(&self) -> Option> { + Some(vec![ + CodeActionKind::QUICKFIX, + CodeActionKind::REFACTOR, + CodeActionKind::REFACTOR_EXTRACT, + CodeActionKind::SOURCE, + ]) + } + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + 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, + _: &Arc, + ) -> Result> { + 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, + _: &Arc, + _cx: &mut AsyncAppContext, + ) -> Result { + Ok(json!({ + "typescript": { + "suggest": { + "completeFunctionCalls": true + } + } + })) + } + + fn language_ids(&self) -> HashMap { + 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 { + 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() +}