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