430 lines · 14.1 KB · Rust
RawDownload
use anyhow::Result;use async_trait::async_trait;use collections::HashMap;use gpui::AsyncApp;use language::{    LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, PromptResponseContext, Toolchain,};use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};use node_runtime::{NodeRuntime, VersionStrategy};use project::{Fs, lsp_store::language_server_settings};use regex::Regex;use semver::Version;use serde_json::Value;use serde_json::json;use settings::update_settings_file;use std::{    ffi::OsString,    future::Future,    path::{Path, PathBuf},    sync::{Arc, LazyLock},};use util::{ResultExt, maybe, merge_json_value_into}; const ACTION_ALWAYS: &str = "Always";const ACTION_NEVER: &str = "Never";const UPDATE_IMPORTS_MESSAGE_PATTERN: &str = "Update imports for";const VTSLS_SERVER_NAME: &str = "vtsls"; fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {    vec![server_path.into(), "--stdio".into()]} pub struct VtslsLspAdapter {    node: NodeRuntime,    fs: Arc<dyn Fs>,} impl VtslsLspAdapter {    const PACKAGE_NAME: &'static str = "@vtsls/language-server";    const SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";     const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib";    const TYPESCRIPT_YARN_TSDK_PATH: &'static str = ".yarn/sdks/typescript/lib";     pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {        VtslsLspAdapter { node, fs }    }     async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {        let yarn_sdk = adapter            .worktree_root_path()            .join(Self::TYPESCRIPT_YARN_TSDK_PATH);         let tsdk_path = if self.fs.is_dir(&yarn_sdk).await {            Self::TYPESCRIPT_YARN_TSDK_PATH        } else {            Self::TYPESCRIPT_TSDK_PATH        };         // vtsls doesn't support TypeScript 7+, which no longer ships `tsserver.js`.        if self            .fs            .is_file(                &adapter                    .worktree_root_path()                    .join(tsdk_path)                    .join("tsserver.js"),            )            .await        {            Some(tsdk_path)        } else {            None        }    }     pub fn enhance_diagnostic_message(message: &str) -> Option<String> {        static SINGLE_WORD_REGEX: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"'([^\s']*)'").expect("Failed to create REGEX"));         static MULTI_WORD_REGEX: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"'([^']+\s+[^']*)'").expect("Failed to create REGEX"));         let first = SINGLE_WORD_REGEX.replace_all(message, "`$1`").to_string();        let second = MULTI_WORD_REGEX            .replace_all(&first, "\n```typescript\n$1\n```\n")            .to_string();        Some(second)    }} const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls"); impl LspInstaller for VtslsLspAdapter {    type BinaryVersion = Version;     async fn fetch_latest_server_version(        &self,        _: &Arc<dyn LspAdapterDelegate>,        _: bool,        _: &mut AsyncApp,    ) -> Result<Self::BinaryVersion> {        self.node            .npm_package_latest_version(Self::PACKAGE_NAME)            .await    }     async fn check_if_user_installed(        &self,        delegate: &Arc<dyn LspAdapterDelegate>,        _: Option<Toolchain>,        _: &AsyncApp,    ) -> Option<LanguageServerBinary> {        let env = delegate.shell_env().await;        let path = delegate.which(SERVER_NAME.as_ref()).await?;        Some(LanguageServerBinary {            path: path.clone(),            arguments: typescript_server_binary_arguments(&path),            env: Some(env),        })    }     fn fetch_server_binary(        &self,        _latest_version: Self::BinaryVersion,        container_dir: PathBuf,        _: &Arc<dyn LspAdapterDelegate>,    ) -> impl Send + Future<Output = Result<LanguageServerBinary>> + use<> {        let node = self.node.clone();         async move {            let server_path = container_dir.join(Self::SERVER_PATH);             node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME])                .await?;             Ok(LanguageServerBinary {                path: node.binary_path().await?,                env: None,                arguments: typescript_server_binary_arguments(&server_path),            })        }    }     fn check_if_version_installed(        &self,        version: &Self::BinaryVersion,        container_dir: &PathBuf,        _: &Arc<dyn LspAdapterDelegate>,    ) -> impl Send + Future<Output = Option<LanguageServerBinary>> + use<> {        let node = self.node.clone();        let server_version = version.clone();        let container_dir = container_dir.clone();         async move {            let server_path = container_dir.join(Self::SERVER_PATH);             if node                .should_install_npm_package(                    Self::PACKAGE_NAME,                    &server_path,                    &container_dir,                    VersionStrategy::Latest(&server_version),                )                .await            {                return None;            }             Some(LanguageServerBinary {                path: node.binary_path().await.ok()?,                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_trait(?Send)]impl LspAdapter for VtslsLspAdapter {    fn name(&self) -> LanguageServerName {        SERVER_NAME    }     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 label_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 = if let Some(description) = item            .label_details            .as_ref()            .and_then(|label_details| label_details.description.as_ref())        {            format!("{} {}", item.label, description)        } else if let Some(detail) = &item.detail {            format!("{} {}", item.label, detail)        } else {            item.label.clone()        };        Some(language::CodeLabel::filtered(            text,            label_len,            item.filter_text.as_deref(),            vec![(0..label_len, highlight_id)],        ))    }     async fn workspace_configuration(        self: Arc<Self>,        delegate: &Arc<dyn LspAdapterDelegate>,        _: Option<Toolchain>,        _: Option<Uri>,        cx: &mut AsyncApp,    ) -> Result<Value> {        let tsdk_path = self.tsdk_path(delegate).await;        let config = serde_json::json!({            "tsdk": tsdk_path,            "suggest": {                "completeFunctionCalls": true            },            "inlayHints": {                "parameterNames": {                    "enabled": "all",                    "suppressWhenArgumentMatchesName": false                },                "parameterTypes": {                    "enabled": true                },                "variableTypes": {                    "enabled": true,                    "suppressWhenTypeMatchesName": false                },                "propertyDeclarationTypes": {                    "enabled": true                },                "functionLikeReturnTypes": {                    "enabled": true                },                "enumMemberValues": {                    "enabled": true                }            },            "implementationsCodeLens": {                "enabled": true,                "showOnAllClassMethods": true,                "showOnInterfaceMethods": true            },            "referencesCodeLens": {                "enabled": true,                "showOnAllFunctions": true            },            "tsserver": {                "maxTsServerMemory": 8192            },        });         let mut default_workspace_configuration = serde_json::json!({            "typescript": config,            "javascript": config,            "vtsls": {                "experimental": {                    "completion": {                        "enableServerSideFuzzyMatch": true,                        "entriesLimit": 5000,                    }                },               "autoUseWorkspaceTsdk": true            }        });         let override_options = cx.update(|cx| {            language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)                .and_then(|s| s.settings.clone())        });         if let Some(override_options) = override_options {            merge_json_value_into(override_options, &mut default_workspace_configuration)        }         Ok(default_workspace_configuration)    }     fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {        VtslsLspAdapter::enhance_diagnostic_message(message)    }     fn language_ids(&self) -> HashMap<LanguageName, String> {        HashMap::from_iter([            (LanguageName::new_static("TypeScript"), "typescript".into()),            (LanguageName::new_static("JavaScript"), "javascript".into()),            (LanguageName::new_static("TSX"), "typescriptreact".into()),        ])    }     fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {        let selected_title = context.selected_action.title.as_str();        let is_preference_response =            selected_title == ACTION_ALWAYS || selected_title == ACTION_NEVER;        if !is_preference_response {            return;        }         if context.message.contains(UPDATE_IMPORTS_MESSAGE_PATTERN) {            let setting_value = match selected_title {                ACTION_ALWAYS => "always",                ACTION_NEVER => "never",                _ => return,            };             let settings = json!({                "typescript": {                    "updateImportsOnFileMove": {                        "enabled": setting_value                    }                },                "javascript": {                    "updateImportsOnFileMove": {                        "enabled": setting_value                    }                }            });             let _ = cx.update(|cx| {                update_settings_file(self.fs.clone(), cx, move |content, _| {                    let lsp_settings = content                        .project                        .lsp                        .0                        .entry(VTSLS_SERVER_NAME.into())                        .or_default();                     if let Some(existing) = &mut lsp_settings.settings {                        merge_json_value_into(settings, existing);                    } else {                        lsp_settings.settings = Some(settings);                    }                });            });        }    }} async fn get_cached_ts_server_binary(    container_dir: PathBuf,    node: &NodeRuntime,) -> Option<LanguageServerBinary> {    maybe!(async {        let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);        anyhow::ensure!(            server_path.exists(),            "missing executable in directory {container_dir:?}"        );        Ok(LanguageServerBinary {            path: node.binary_path().await?,            env: None,            arguments: typescript_server_binary_arguments(&server_path),        })    })    .await    .log_err()} #[cfg(test)]mod tests {    use crate::vtsls::VtslsLspAdapter;     #[test]    fn test_diagnostic_message_to_markdown() {        // Leaves simple messages unchanged        let message = "The expected type comes from the return type of this signature.";         let expected = "The expected type comes from the return type of this signature.";         assert_eq!(            VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),            expected        );         // Parses both multi-word and single-word correctly        let message = "Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";         let expected = "Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";         assert_eq!(            VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),            expected        );         // Parses multi-and-single word in any order, and ignores existing newlines        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'.";         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`.";         assert_eq!(            VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),            expected        );    }}