vtsls.rs

  1use anyhow::{anyhow, Result};
  2use async_trait::async_trait;
  3use collections::HashMap;
  4use gpui::AsyncAppContext;
  5use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
  6use lsp::{CodeActionKind, LanguageServerBinary};
  7use node_runtime::NodeRuntime;
  8use project::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::{maybe, merge_json_value_into, ResultExt};
 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 SERVER_PATH: &'static str = "node_modules/@vtsls/language-server/bin/vtsls.js";
 28
 29    pub fn new(node: NodeRuntime) -> Self {
 30        VtslsLspAdapter { node }
 31    }
 32    async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
 33        let is_yarn = adapter
 34            .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
 35            .await
 36            .is_ok();
 37
 38        if is_yarn {
 39            ".yarn/sdks/typescript/lib"
 40        } else {
 41            "node_modules/typescript/lib"
 42        }
 43    }
 44}
 45
 46struct TypeScriptVersions {
 47    typescript_version: String,
 48    server_version: String,
 49}
 50
 51const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
 52#[async_trait(?Send)]
 53impl LspAdapter for VtslsLspAdapter {
 54    fn name(&self) -> LanguageServerName {
 55        SERVER_NAME.clone()
 56    }
 57
 58    async fn fetch_latest_server_version(
 59        &self,
 60        _: &dyn LspAdapterDelegate,
 61    ) -> Result<Box<dyn 'static + Send + Any>> {
 62        Ok(Box::new(TypeScriptVersions {
 63            typescript_version: self.node.npm_package_latest_version("typescript").await?,
 64            server_version: self
 65                .node
 66                .npm_package_latest_version("@vtsls/language-server")
 67                .await?,
 68        }) as Box<_>)
 69    }
 70
 71    async fn check_if_user_installed(
 72        &self,
 73        delegate: &dyn LspAdapterDelegate,
 74        _: &AsyncAppContext,
 75    ) -> Option<LanguageServerBinary> {
 76        let env = delegate.shell_env().await;
 77        let path = delegate.which(SERVER_NAME.as_ref()).await?;
 78        Some(LanguageServerBinary {
 79            path: path.clone(),
 80            arguments: typescript_server_binary_arguments(&path),
 81            env: Some(env),
 82        })
 83    }
 84
 85    async fn fetch_server_binary(
 86        &self,
 87        latest_version: Box<dyn 'static + Send + Any>,
 88        container_dir: PathBuf,
 89        _: &dyn LspAdapterDelegate,
 90    ) -> Result<LanguageServerBinary> {
 91        let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
 92        let server_path = container_dir.join(Self::SERVER_PATH);
 93        let package_name = "typescript";
 94
 95        let should_install_language_server = self
 96            .node
 97            .should_install_npm_package(
 98                package_name,
 99                &server_path,
100                &container_dir,
101                latest_version.typescript_version.as_str(),
102            )
103            .await;
104
105        if should_install_language_server {
106            self.node
107                .npm_install_packages(
108                    &container_dir,
109                    &[
110                        (package_name, latest_version.typescript_version.as_str()),
111                        (
112                            "@vtsls/language-server",
113                            latest_version.server_version.as_str(),
114                        ),
115                    ],
116                )
117                .await?;
118        }
119
120        Ok(LanguageServerBinary {
121            path: self.node.binary_path().await?,
122            env: None,
123            arguments: typescript_server_binary_arguments(&server_path),
124        })
125    }
126
127    async fn cached_server_binary(
128        &self,
129        container_dir: PathBuf,
130        _: &dyn LspAdapterDelegate,
131    ) -> Option<LanguageServerBinary> {
132        get_cached_ts_server_binary(container_dir, &self.node).await
133    }
134
135    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
136        Some(vec![
137            CodeActionKind::QUICKFIX,
138            CodeActionKind::REFACTOR,
139            CodeActionKind::REFACTOR_EXTRACT,
140            CodeActionKind::SOURCE,
141        ])
142    }
143
144    async fn label_for_completion(
145        &self,
146        item: &lsp::CompletionItem,
147        language: &Arc<language::Language>,
148    ) -> Option<language::CodeLabel> {
149        use lsp::CompletionItemKind as Kind;
150        let len = item.label.len();
151        let grammar = language.grammar()?;
152        let highlight_id = match item.kind? {
153            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
154            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
155            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
156            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
157            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
158            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
159            _ => None,
160        }?;
161
162        let one_line = |s: &str| s.replace("    ", "").replace('\n', " ");
163
164        let text = if let Some(description) = item
165            .label_details
166            .as_ref()
167            .and_then(|label_details| label_details.description.as_ref())
168        {
169            format!("{} {}", item.label, one_line(description))
170        } else if let Some(detail) = &item.detail {
171            format!("{} {}", item.label, one_line(detail))
172        } else {
173            item.label.clone()
174        };
175
176        Some(language::CodeLabel {
177            text,
178            runs: vec![(0..len, highlight_id)],
179            filter_range: 0..len,
180        })
181    }
182
183    async fn workspace_configuration(
184        self: Arc<Self>,
185        delegate: &Arc<dyn LspAdapterDelegate>,
186        _: Arc<dyn LanguageToolchainStore>,
187        cx: &mut AsyncAppContext,
188    ) -> Result<Value> {
189        let tsdk_path = Self::tsdk_path(delegate).await;
190        let config = serde_json::json!({
191            "tsdk": tsdk_path,
192            "suggest": {
193                "completeFunctionCalls": true
194            },
195            "inlayHints": {
196                "parameterNames": {
197                    "enabled": "all",
198                    "suppressWhenArgumentMatchesName": false
199                },
200                "parameterTypes": {
201                    "enabled": true
202                },
203                "variableTypes": {
204                    "enabled": true,
205                    "suppressWhenTypeMatchesName": false
206                },
207                "propertyDeclarationTypes": {
208                    "enabled": true
209                },
210                "functionLikeReturnTypes": {
211                    "enabled": true
212                },
213                "enumMemberValues": {
214                    "enabled": true
215                }
216            },
217            "tsserver": {
218                "maxTsServerMemory": 8092
219            },
220        });
221
222        let mut default_workspace_configuration = serde_json::json!({
223            "typescript": config,
224            "javascript": config,
225            "vtsls": {
226                "experimental": {
227                    "completion": {
228                        "enableServerSideFuzzyMatch": true,
229                        "entriesLimit": 5000,
230                    }
231                },
232               "autoUseWorkspaceTsdk": true
233            }
234        });
235
236        let override_options = cx.update(|cx| {
237            language_server_settings(delegate.as_ref(), &SERVER_NAME, cx)
238                .and_then(|s| s.settings.clone())
239        })?;
240
241        if let Some(override_options) = override_options {
242            merge_json_value_into(override_options, &mut default_workspace_configuration)
243        }
244
245        Ok(default_workspace_configuration)
246    }
247
248    fn language_ids(&self) -> HashMap<String, String> {
249        HashMap::from_iter([
250            ("TypeScript".into(), "typescript".into()),
251            ("JavaScript".into(), "javascript".into()),
252            ("TSX".into(), "typescriptreact".into()),
253        ])
254    }
255}
256
257async fn get_cached_ts_server_binary(
258    container_dir: PathBuf,
259    node: &NodeRuntime,
260) -> Option<LanguageServerBinary> {
261    maybe!(async {
262        let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
263        if server_path.exists() {
264            Ok(LanguageServerBinary {
265                path: node.binary_path().await?,
266                env: None,
267                arguments: typescript_server_binary_arguments(&server_path),
268            })
269        } else {
270            Err(anyhow!(
271                "missing executable in directory {:?}",
272                container_dir
273            ))
274        }
275    })
276    .await
277    .log_err()
278}