vtsls.rs

  1use anyhow::{anyhow, Result};
  2use async_trait::async_trait;
  3use collections::HashMap;
  4use gpui::AsyncAppContext;
  5use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
  6use lsp::{CodeActionKind, LanguageServerBinary};
  7use node_runtime::NodeRuntime;
  8use project::{lsp_store::language_server_settings, project_settings::BinarySettings};
  9use serde_json::{json, Value};
 10use std::{
 11    any::Any,
 12    ffi::OsString,
 13    path::{Path, PathBuf},
 14    sync::Arc,
 15};
 16use util::{maybe, 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: Arc<dyn 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: Arc<dyn 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: &str = "vtsls";
 52#[async_trait(?Send)]
 53impl LspAdapter for VtslsLspAdapter {
 54    fn name(&self) -> LanguageServerName {
 55        LanguageServerName(SERVER_NAME.into())
 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        cx: &AsyncAppContext,
 75    ) -> Option<LanguageServerBinary> {
 76        let configured_binary = cx.update(|cx| {
 77            language_server_settings(delegate, SERVER_NAME, cx).and_then(|s| s.binary.clone())
 78        });
 79
 80        match configured_binary {
 81            Ok(Some(BinarySettings {
 82                path: Some(path),
 83                arguments,
 84                ..
 85            })) => Some(LanguageServerBinary {
 86                path: path.into(),
 87                arguments: arguments
 88                    .unwrap_or_default()
 89                    .iter()
 90                    .map(|arg| arg.into())
 91                    .collect(),
 92                env: None,
 93            }),
 94            Ok(Some(BinarySettings {
 95                path_lookup: Some(false),
 96                ..
 97            })) => None,
 98            _ => {
 99                let env = delegate.shell_env().await;
100                let path = delegate.which(SERVER_NAME.as_ref()).await?;
101                Some(LanguageServerBinary {
102                    path: path.clone(),
103                    arguments: typescript_server_binary_arguments(&path),
104                    env: Some(env),
105                })
106            }
107        }
108    }
109
110    async fn fetch_server_binary(
111        &self,
112        latest_version: Box<dyn 'static + Send + Any>,
113        container_dir: PathBuf,
114        _: &dyn LspAdapterDelegate,
115    ) -> Result<LanguageServerBinary> {
116        let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
117        let server_path = container_dir.join(Self::SERVER_PATH);
118        let package_name = "typescript";
119
120        let should_install_language_server = self
121            .node
122            .should_install_npm_package(
123                package_name,
124                &server_path,
125                &container_dir,
126                latest_version.typescript_version.as_str(),
127            )
128            .await;
129
130        if should_install_language_server {
131            self.node
132                .npm_install_packages(
133                    &container_dir,
134                    &[
135                        (package_name, latest_version.typescript_version.as_str()),
136                        (
137                            "@vtsls/language-server",
138                            latest_version.server_version.as_str(),
139                        ),
140                    ],
141                )
142                .await?;
143        }
144
145        Ok(LanguageServerBinary {
146            path: self.node.binary_path().await?,
147            env: None,
148            arguments: typescript_server_binary_arguments(&server_path),
149        })
150    }
151
152    async fn cached_server_binary(
153        &self,
154        container_dir: PathBuf,
155        _: &dyn LspAdapterDelegate,
156    ) -> Option<LanguageServerBinary> {
157        get_cached_ts_server_binary(container_dir, &*self.node).await
158    }
159
160    async fn installation_test_binary(
161        &self,
162        container_dir: PathBuf,
163    ) -> Option<LanguageServerBinary> {
164        get_cached_ts_server_binary(container_dir, &*self.node).await
165    }
166
167    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
168        Some(vec![
169            CodeActionKind::QUICKFIX,
170            CodeActionKind::REFACTOR,
171            CodeActionKind::REFACTOR_EXTRACT,
172            CodeActionKind::SOURCE,
173        ])
174    }
175
176    async fn label_for_completion(
177        &self,
178        item: &lsp::CompletionItem,
179        language: &Arc<language::Language>,
180    ) -> Option<language::CodeLabel> {
181        use lsp::CompletionItemKind as Kind;
182        let len = item.label.len();
183        let grammar = language.grammar()?;
184        let highlight_id = match item.kind? {
185            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
186            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
187            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
188            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
189            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
190            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
191            _ => None,
192        }?;
193
194        let one_line = |s: &str| s.replace("    ", "").replace('\n', " ");
195
196        let text = if let Some(description) = item
197            .label_details
198            .as_ref()
199            .and_then(|label_details| label_details.description.as_ref())
200        {
201            format!("{} {}", item.label, one_line(description))
202        } else if let Some(detail) = &item.detail {
203            format!("{} {}", item.label, one_line(detail))
204        } else {
205            item.label.clone()
206        };
207
208        Some(language::CodeLabel {
209            text,
210            runs: vec![(0..len, highlight_id)],
211            filter_range: 0..len,
212        })
213    }
214
215    async fn initialization_options(
216        self: Arc<Self>,
217        adapter: &Arc<dyn LspAdapterDelegate>,
218    ) -> Result<Option<serde_json::Value>> {
219        let tsdk_path = Self::tsdk_path(adapter).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        });
248
249        Ok(Some(json!({
250            "typescript": config,
251            "javascript": config,
252            "vtsls": {
253                "experimental": {
254                    "completion": {
255                        "enableServerSideFuzzyMatch": true,
256                        "entriesLimit": 5000,
257                    }
258                },
259               "autoUseWorkspaceTsdk": true
260            }
261        })))
262    }
263
264    async fn workspace_configuration(
265        self: Arc<Self>,
266        delegate: &Arc<dyn LspAdapterDelegate>,
267        cx: &mut AsyncAppContext,
268    ) -> Result<Value> {
269        let override_options = cx.update(|cx| {
270            language_server_settings(delegate.as_ref(), SERVER_NAME, cx)
271                .and_then(|s| s.settings.clone())
272        })?;
273
274        if let Some(options) = override_options {
275            return Ok(options);
276        }
277
278        let config = serde_json::json!({
279            "tsserver": {
280                "maxTsServerMemory": 8092
281            },
282        });
283
284        Ok(serde_json::json!({
285            "typescript": config,
286            "javascript": config
287        }))
288    }
289
290    fn language_ids(&self) -> HashMap<String, String> {
291        HashMap::from_iter([
292            ("TypeScript".into(), "typescript".into()),
293            ("JavaScript".into(), "javascript".into()),
294            ("TSX".into(), "typescriptreact".into()),
295        ])
296    }
297}
298
299async fn get_cached_ts_server_binary(
300    container_dir: PathBuf,
301    node: &dyn NodeRuntime,
302) -> Option<LanguageServerBinary> {
303    maybe!(async {
304        let server_path = container_dir.join(VtslsLspAdapter::SERVER_PATH);
305        if server_path.exists() {
306            Ok(LanguageServerBinary {
307                path: node.binary_path().await?,
308                env: None,
309                arguments: typescript_server_binary_arguments(&server_path),
310            })
311        } else {
312            Err(anyhow!(
313                "missing executable in directory {:?}",
314                container_dir
315            ))
316        }
317    })
318    .await
319    .log_err()
320}