typescript.rs

  1use anyhow::{anyhow, Result};
  2use async_compression::futures::bufread::GzipDecoder;
  3use async_tar::Archive;
  4use async_trait::async_trait;
  5use collections::HashMap;
  6use gpui::AppContext;
  7use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
  8use lsp::{CodeActionKind, LanguageServerBinary};
  9use node_runtime::NodeRuntime;
 10use serde_json::{json, Value};
 11use smol::{fs, io::BufReader, stream::StreamExt};
 12use std::{
 13    any::Any,
 14    ffi::OsString,
 15    path::{Path, PathBuf},
 16    sync::Arc,
 17};
 18use util::{
 19    async_maybe,
 20    fs::remove_matching,
 21    github::{latest_github_release, GitHubLspBinaryVersion},
 22    ResultExt,
 23};
 24
 25fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 26    vec![server_path.into(), "--stdio".into()]
 27}
 28
 29fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 30    vec![server_path.into(), "--stdio".into()]
 31}
 32
 33pub struct TypeScriptLspAdapter {
 34    node: Arc<dyn NodeRuntime>,
 35}
 36
 37impl TypeScriptLspAdapter {
 38    const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
 39    const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
 40
 41    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
 42        TypeScriptLspAdapter { node }
 43    }
 44}
 45
 46struct TypeScriptVersions {
 47    typescript_version: String,
 48    server_version: String,
 49}
 50
 51#[async_trait]
 52impl LspAdapter for TypeScriptLspAdapter {
 53    fn name(&self) -> LanguageServerName {
 54        LanguageServerName("typescript-language-server".into())
 55    }
 56
 57    fn short_name(&self) -> &'static str {
 58        "tsserver"
 59    }
 60
 61    async fn fetch_latest_server_version(
 62        &self,
 63        _: &dyn LspAdapterDelegate,
 64    ) -> Result<Box<dyn 'static + Send + Any>> {
 65        Ok(Box::new(TypeScriptVersions {
 66            typescript_version: self.node.npm_package_latest_version("typescript").await?,
 67            server_version: self
 68                .node
 69                .npm_package_latest_version("typescript-language-server")
 70                .await?,
 71        }) as Box<_>)
 72    }
 73
 74    async fn fetch_server_binary(
 75        &self,
 76        version: Box<dyn 'static + Send + Any>,
 77        container_dir: PathBuf,
 78        _: &dyn LspAdapterDelegate,
 79    ) -> Result<LanguageServerBinary> {
 80        let version = version.downcast::<TypeScriptVersions>().unwrap();
 81        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 82
 83        if fs::metadata(&server_path).await.is_err() {
 84            self.node
 85                .npm_install_packages(
 86                    &container_dir,
 87                    &[
 88                        ("typescript", version.typescript_version.as_str()),
 89                        (
 90                            "typescript-language-server",
 91                            version.server_version.as_str(),
 92                        ),
 93                    ],
 94                )
 95                .await?;
 96        }
 97
 98        Ok(LanguageServerBinary {
 99            path: self.node.binary_path().await?,
100            arguments: typescript_server_binary_arguments(&server_path),
101        })
102    }
103
104    async fn cached_server_binary(
105        &self,
106        container_dir: PathBuf,
107        _: &dyn LspAdapterDelegate,
108    ) -> Option<LanguageServerBinary> {
109        get_cached_ts_server_binary(container_dir, &*self.node).await
110    }
111
112    async fn installation_test_binary(
113        &self,
114        container_dir: PathBuf,
115    ) -> Option<LanguageServerBinary> {
116        get_cached_ts_server_binary(container_dir, &*self.node).await
117    }
118
119    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
120        Some(vec![
121            CodeActionKind::QUICKFIX,
122            CodeActionKind::REFACTOR,
123            CodeActionKind::REFACTOR_EXTRACT,
124            CodeActionKind::SOURCE,
125        ])
126    }
127
128    async fn label_for_completion(
129        &self,
130        item: &lsp::CompletionItem,
131        language: &Arc<language::Language>,
132    ) -> Option<language::CodeLabel> {
133        use lsp::CompletionItemKind as Kind;
134        let len = item.label.len();
135        let grammar = language.grammar()?;
136        let highlight_id = match item.kind? {
137            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
138            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
139            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
140            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
141            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
142            _ => None,
143        }?;
144
145        let text = match &item.detail {
146            Some(detail) => format!("{} {}", item.label, detail),
147            None => item.label.clone(),
148        };
149
150        Some(language::CodeLabel {
151            text,
152            runs: vec![(0..len, highlight_id)],
153            filter_range: 0..len,
154        })
155    }
156
157    fn initialization_options(&self) -> Option<serde_json::Value> {
158        Some(json!({
159            "provideFormatter": true,
160            "tsserver": {
161                "path": "node_modules/typescript/lib",
162            },
163            "preferences": {
164                "includeInlayParameterNameHints": "all",
165                "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
166                "includeInlayFunctionParameterTypeHints": true,
167                "includeInlayVariableTypeHints": true,
168                "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
169                "includeInlayPropertyDeclarationTypeHints": true,
170                "includeInlayFunctionLikeReturnTypeHints": true,
171                "includeInlayEnumMemberValueHints": true,
172            }
173        }))
174    }
175
176    fn language_ids(&self) -> HashMap<String, String> {
177        HashMap::from_iter([
178            ("TypeScript".into(), "typescript".into()),
179            ("JavaScript".into(), "javascript".into()),
180            ("TSX".into(), "typescriptreact".into()),
181        ])
182    }
183}
184
185async fn get_cached_ts_server_binary(
186    container_dir: PathBuf,
187    node: &dyn NodeRuntime,
188) -> Option<LanguageServerBinary> {
189    async_maybe!({
190        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
191        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
192        if new_server_path.exists() {
193            Ok(LanguageServerBinary {
194                path: node.binary_path().await?,
195                arguments: typescript_server_binary_arguments(&new_server_path),
196            })
197        } else if old_server_path.exists() {
198            Ok(LanguageServerBinary {
199                path: node.binary_path().await?,
200                arguments: typescript_server_binary_arguments(&old_server_path),
201            })
202        } else {
203            Err(anyhow!(
204                "missing executable in directory {:?}",
205                container_dir
206            ))
207        }
208    })
209    .await
210    .log_err()
211}
212
213pub struct EsLintLspAdapter {
214    node: Arc<dyn NodeRuntime>,
215}
216
217impl EsLintLspAdapter {
218    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
219
220    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
221        EsLintLspAdapter { node }
222    }
223}
224
225#[async_trait]
226impl LspAdapter for EsLintLspAdapter {
227    fn workspace_configuration(&self, workspace_root: &Path, _: &mut AppContext) -> Value {
228        json!({
229            "": {
230                "validate": "on",
231                "rulesCustomizations": [],
232                "run": "onType",
233                "nodePath": null,
234                "workingDirectory": {"mode": "auto"},
235                "workspaceFolder": {
236                    "uri": workspace_root,
237                    "name": workspace_root.file_name()
238                        .unwrap_or_else(|| workspace_root.as_os_str()),
239                },
240            }
241        })
242    }
243
244    fn name(&self) -> LanguageServerName {
245        LanguageServerName("eslint".into())
246    }
247
248    fn short_name(&self) -> &'static str {
249        "eslint"
250    }
251
252    async fn fetch_latest_server_version(
253        &self,
254        delegate: &dyn LspAdapterDelegate,
255    ) -> Result<Box<dyn 'static + Send + Any>> {
256        // At the time of writing the latest vscode-eslint release was released in 2020 and requires
257        // special custom LSP protocol extensions be handled to fully initialize. Download the latest
258        // prerelease instead to sidestep this issue
259        let release = latest_github_release(
260            "microsoft/vscode-eslint",
261            false,
262            false,
263            delegate.http_client(),
264        )
265        .await?;
266        Ok(Box::new(GitHubLspBinaryVersion {
267            name: release.tag_name,
268            url: release.tarball_url,
269        }))
270    }
271
272    async fn fetch_server_binary(
273        &self,
274        version: Box<dyn 'static + Send + Any>,
275        container_dir: PathBuf,
276        delegate: &dyn LspAdapterDelegate,
277    ) -> Result<LanguageServerBinary> {
278        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
279        let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
280        let server_path = destination_path.join(Self::SERVER_PATH);
281
282        if fs::metadata(&server_path).await.is_err() {
283            remove_matching(&container_dir, |entry| entry != destination_path).await;
284
285            let mut response = delegate
286                .http_client()
287                .get(&version.url, Default::default(), true)
288                .await
289                .map_err(|err| anyhow!("error downloading release: {}", err))?;
290            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
291            let archive = Archive::new(decompressed_bytes);
292            archive.unpack(&destination_path).await?;
293
294            let mut dir = fs::read_dir(&destination_path).await?;
295            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
296            let repo_root = destination_path.join("vscode-eslint");
297            fs::rename(first.path(), &repo_root).await?;
298
299            self.node
300                .run_npm_subcommand(Some(&repo_root), "install", &[])
301                .await?;
302
303            self.node
304                .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
305                .await?;
306        }
307
308        Ok(LanguageServerBinary {
309            path: self.node.binary_path().await?,
310            arguments: eslint_server_binary_arguments(&server_path),
311        })
312    }
313
314    async fn cached_server_binary(
315        &self,
316        container_dir: PathBuf,
317        _: &dyn LspAdapterDelegate,
318    ) -> Option<LanguageServerBinary> {
319        get_cached_eslint_server_binary(container_dir, &*self.node).await
320    }
321
322    async fn installation_test_binary(
323        &self,
324        container_dir: PathBuf,
325    ) -> Option<LanguageServerBinary> {
326        get_cached_eslint_server_binary(container_dir, &*self.node).await
327    }
328
329    async fn label_for_completion(
330        &self,
331        _item: &lsp::CompletionItem,
332        _language: &Arc<language::Language>,
333    ) -> Option<language::CodeLabel> {
334        None
335    }
336
337    fn initialization_options(&self) -> Option<serde_json::Value> {
338        None
339    }
340}
341
342async fn get_cached_eslint_server_binary(
343    container_dir: PathBuf,
344    node: &dyn NodeRuntime,
345) -> Option<LanguageServerBinary> {
346    async_maybe!({
347        // This is unfortunate but we don't know what the version is to build a path directly
348        let mut dir = fs::read_dir(&container_dir).await?;
349        let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
350        if !first.file_type().await?.is_dir() {
351            return Err(anyhow!("First entry is not a directory"));
352        }
353        let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
354
355        Ok(LanguageServerBinary {
356            path: node.binary_path().await?,
357            arguments: eslint_server_binary_arguments(&server_path),
358        })
359    })
360    .await
361    .log_err()
362}
363
364#[cfg(test)]
365mod tests {
366    use gpui::{Context, TestAppContext};
367    use text::BufferId;
368    use unindent::Unindent;
369
370    #[gpui::test]
371    async fn test_outline(cx: &mut TestAppContext) {
372        let language = crate::languages::language(
373            "typescript",
374            tree_sitter_typescript::language_typescript(),
375            None,
376        )
377        .await;
378
379        let text = r#"
380            function a() {
381              // local variables are omitted
382              let a1 = 1;
383              // all functions are included
384              async function a2() {}
385            }
386            // top-level variables are included
387            let b: C
388            function getB() {}
389            // exported variables are included
390            export const d = e;
391        "#
392        .unindent();
393
394        let buffer = cx.new_model(|cx| {
395            language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
396                .with_language(language, cx)
397        });
398        let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
399        assert_eq!(
400            outline
401                .items
402                .iter()
403                .map(|item| (item.text.as_str(), item.depth))
404                .collect::<Vec<_>>(),
405            &[
406                ("function a()", 0),
407                ("async function a2()", 1),
408                ("let b", 0),
409                ("function getB()", 0),
410                ("const d", 0),
411            ]
412        );
413    }
414}