vtsls.rs

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