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            env: None,
101            arguments: typescript_server_binary_arguments(&server_path),
102        })
103    }
104
105    async fn cached_server_binary(
106        &self,
107        container_dir: PathBuf,
108        _: &dyn LspAdapterDelegate,
109    ) -> Option<LanguageServerBinary> {
110        get_cached_ts_server_binary(container_dir, &*self.node).await
111    }
112
113    async fn installation_test_binary(
114        &self,
115        container_dir: PathBuf,
116    ) -> Option<LanguageServerBinary> {
117        get_cached_ts_server_binary(container_dir, &*self.node).await
118    }
119
120    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
121        Some(vec![
122            CodeActionKind::QUICKFIX,
123            CodeActionKind::REFACTOR,
124            CodeActionKind::REFACTOR_EXTRACT,
125            CodeActionKind::SOURCE,
126        ])
127    }
128
129    async fn label_for_completion(
130        &self,
131        item: &lsp::CompletionItem,
132        language: &Arc<language::Language>,
133    ) -> Option<language::CodeLabel> {
134        use lsp::CompletionItemKind as Kind;
135        let len = item.label.len();
136        let grammar = language.grammar()?;
137        let highlight_id = match item.kind? {
138            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
139            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
140            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
141            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
142            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
143            _ => None,
144        }?;
145
146        let text = match &item.detail {
147            Some(detail) => format!("{} {}", item.label, detail),
148            None => item.label.clone(),
149        };
150
151        Some(language::CodeLabel {
152            text,
153            runs: vec![(0..len, highlight_id)],
154            filter_range: 0..len,
155        })
156    }
157
158    fn initialization_options(&self) -> Option<serde_json::Value> {
159        Some(json!({
160            "provideFormatter": true,
161            "tsserver": {
162                "path": "node_modules/typescript/lib",
163            },
164            "preferences": {
165                "includeInlayParameterNameHints": "all",
166                "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
167                "includeInlayFunctionParameterTypeHints": true,
168                "includeInlayVariableTypeHints": true,
169                "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
170                "includeInlayPropertyDeclarationTypeHints": true,
171                "includeInlayFunctionLikeReturnTypeHints": true,
172                "includeInlayEnumMemberValueHints": true,
173            }
174        }))
175    }
176
177    fn language_ids(&self) -> HashMap<String, String> {
178        HashMap::from_iter([
179            ("TypeScript".into(), "typescript".into()),
180            ("JavaScript".into(), "javascript".into()),
181            ("TSX".into(), "typescriptreact".into()),
182        ])
183    }
184}
185
186async fn get_cached_ts_server_binary(
187    container_dir: PathBuf,
188    node: &dyn NodeRuntime,
189) -> Option<LanguageServerBinary> {
190    async_maybe!({
191        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
192        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
193        if new_server_path.exists() {
194            Ok(LanguageServerBinary {
195                path: node.binary_path().await?,
196                env: None,
197                arguments: typescript_server_binary_arguments(&new_server_path),
198            })
199        } else if old_server_path.exists() {
200            Ok(LanguageServerBinary {
201                path: node.binary_path().await?,
202                env: None,
203                arguments: typescript_server_binary_arguments(&old_server_path),
204            })
205        } else {
206            Err(anyhow!(
207                "missing executable in directory {:?}",
208                container_dir
209            ))
210        }
211    })
212    .await
213    .log_err()
214}
215
216pub struct EsLintLspAdapter {
217    node: Arc<dyn NodeRuntime>,
218}
219
220impl EsLintLspAdapter {
221    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
222
223    pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
224        EsLintLspAdapter { node }
225    }
226}
227
228#[async_trait]
229impl LspAdapter for EsLintLspAdapter {
230    fn workspace_configuration(&self, workspace_root: &Path, _: &mut AppContext) -> Value {
231        json!({
232            "": {
233                "validate": "on",
234                "rulesCustomizations": [],
235                "run": "onType",
236                "nodePath": null,
237                "workingDirectory": {"mode": "auto"},
238                "workspaceFolder": {
239                    "uri": workspace_root,
240                    "name": workspace_root.file_name()
241                        .unwrap_or_else(|| workspace_root.as_os_str()),
242                },
243                "problems": {},
244                "codeActionOnSave": {
245                    // We enable this, but without also configuring `code_actions_on_format`
246                    // in the Zed configuration, it doesn't have an effect.
247                    "enable": true,
248                },
249                "experimental": {
250                    "useFlatConfig": workspace_root.join("eslint.config.js").is_file(),
251                },
252            }
253        })
254    }
255
256    fn name(&self) -> LanguageServerName {
257        LanguageServerName("eslint".into())
258    }
259
260    fn short_name(&self) -> &'static str {
261        "eslint"
262    }
263
264    async fn fetch_latest_server_version(
265        &self,
266        delegate: &dyn LspAdapterDelegate,
267    ) -> Result<Box<dyn 'static + Send + Any>> {
268        // At the time of writing the latest vscode-eslint release was released in 2020 and requires
269        // special custom LSP protocol extensions be handled to fully initialize. Download the latest
270        // prerelease instead to sidestep this issue
271        let release = latest_github_release(
272            "microsoft/vscode-eslint",
273            false,
274            true,
275            delegate.http_client(),
276        )
277        .await?;
278        Ok(Box::new(GitHubLspBinaryVersion {
279            name: release.tag_name,
280            url: release.tarball_url,
281        }))
282    }
283
284    async fn fetch_server_binary(
285        &self,
286        version: Box<dyn 'static + Send + Any>,
287        container_dir: PathBuf,
288        delegate: &dyn LspAdapterDelegate,
289    ) -> Result<LanguageServerBinary> {
290        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
291        let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
292        let server_path = destination_path.join(Self::SERVER_PATH);
293
294        if fs::metadata(&server_path).await.is_err() {
295            remove_matching(&container_dir, |entry| entry != destination_path).await;
296
297            let mut response = delegate
298                .http_client()
299                .get(&version.url, Default::default(), true)
300                .await
301                .map_err(|err| anyhow!("error downloading release: {}", err))?;
302            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
303            let archive = Archive::new(decompressed_bytes);
304            archive.unpack(&destination_path).await?;
305
306            let mut dir = fs::read_dir(&destination_path).await?;
307            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
308            let repo_root = destination_path.join("vscode-eslint");
309            fs::rename(first.path(), &repo_root).await?;
310
311            self.node
312                .run_npm_subcommand(Some(&repo_root), "install", &[])
313                .await?;
314
315            self.node
316                .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
317                .await?;
318        }
319
320        Ok(LanguageServerBinary {
321            path: self.node.binary_path().await?,
322            env: None,
323            arguments: eslint_server_binary_arguments(&server_path),
324        })
325    }
326
327    async fn cached_server_binary(
328        &self,
329        container_dir: PathBuf,
330        _: &dyn LspAdapterDelegate,
331    ) -> Option<LanguageServerBinary> {
332        get_cached_eslint_server_binary(container_dir, &*self.node).await
333    }
334
335    async fn installation_test_binary(
336        &self,
337        container_dir: PathBuf,
338    ) -> Option<LanguageServerBinary> {
339        get_cached_eslint_server_binary(container_dir, &*self.node).await
340    }
341
342    async fn label_for_completion(
343        &self,
344        _item: &lsp::CompletionItem,
345        _language: &Arc<language::Language>,
346    ) -> Option<language::CodeLabel> {
347        None
348    }
349
350    fn initialization_options(&self) -> Option<serde_json::Value> {
351        None
352    }
353}
354
355async fn get_cached_eslint_server_binary(
356    container_dir: PathBuf,
357    node: &dyn NodeRuntime,
358) -> Option<LanguageServerBinary> {
359    async_maybe!({
360        // This is unfortunate but we don't know what the version is to build a path directly
361        let mut dir = fs::read_dir(&container_dir).await?;
362        let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
363        if !first.file_type().await?.is_dir() {
364            return Err(anyhow!("First entry is not a directory"));
365        }
366        let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
367
368        Ok(LanguageServerBinary {
369            path: node.binary_path().await?,
370            env: None,
371            arguments: eslint_server_binary_arguments(&server_path),
372        })
373    })
374    .await
375    .log_err()
376}
377
378#[cfg(test)]
379mod tests {
380    use gpui::{Context, TestAppContext};
381    use text::BufferId;
382    use unindent::Unindent;
383
384    #[gpui::test]
385    async fn test_outline(cx: &mut TestAppContext) {
386        let language = crate::language(
387            "typescript",
388            tree_sitter_typescript::language_typescript(),
389            None,
390        )
391        .await;
392
393        let text = r#"
394            function a() {
395              // local variables are omitted
396              let a1 = 1;
397              // all functions are included
398              async function a2() {}
399            }
400            // top-level variables are included
401            let b: C
402            function getB() {}
403            // exported variables are included
404            export const d = e;
405        "#
406        .unindent();
407
408        let buffer = cx.new_model(|cx| {
409            language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
410                .with_language(language, cx)
411        });
412        let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
413        assert_eq!(
414            outline
415                .items
416                .iter()
417                .map(|item| (item.text.as_str(), item.depth))
418                .collect::<Vec<_>>(),
419            &[
420                ("function a()", 0),
421                ("async function a2()", 1),
422                ("let b", 0),
423                ("function getB()", 0),
424                ("const d", 0),
425            ]
426        );
427    }
428}