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                "experimental": {
245                    "useFlatConfig": workspace_root.join("eslint.config.js").is_file(),
246                },
247            }
248        })
249    }
250
251    fn name(&self) -> LanguageServerName {
252        LanguageServerName("eslint".into())
253    }
254
255    fn short_name(&self) -> &'static str {
256        "eslint"
257    }
258
259    async fn fetch_latest_server_version(
260        &self,
261        delegate: &dyn LspAdapterDelegate,
262    ) -> Result<Box<dyn 'static + Send + Any>> {
263        // At the time of writing the latest vscode-eslint release was released in 2020 and requires
264        // special custom LSP protocol extensions be handled to fully initialize. Download the latest
265        // prerelease instead to sidestep this issue
266        let release = latest_github_release(
267            "microsoft/vscode-eslint",
268            false,
269            true,
270            delegate.http_client(),
271        )
272        .await?;
273        Ok(Box::new(GitHubLspBinaryVersion {
274            name: release.tag_name,
275            url: release.tarball_url,
276        }))
277    }
278
279    async fn fetch_server_binary(
280        &self,
281        version: Box<dyn 'static + Send + Any>,
282        container_dir: PathBuf,
283        delegate: &dyn LspAdapterDelegate,
284    ) -> Result<LanguageServerBinary> {
285        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
286        let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
287        let server_path = destination_path.join(Self::SERVER_PATH);
288
289        if fs::metadata(&server_path).await.is_err() {
290            remove_matching(&container_dir, |entry| entry != destination_path).await;
291
292            let mut response = delegate
293                .http_client()
294                .get(&version.url, Default::default(), true)
295                .await
296                .map_err(|err| anyhow!("error downloading release: {}", err))?;
297            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
298            let archive = Archive::new(decompressed_bytes);
299            archive.unpack(&destination_path).await?;
300
301            let mut dir = fs::read_dir(&destination_path).await?;
302            let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
303            let repo_root = destination_path.join("vscode-eslint");
304            fs::rename(first.path(), &repo_root).await?;
305
306            self.node
307                .run_npm_subcommand(Some(&repo_root), "install", &[])
308                .await?;
309
310            self.node
311                .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
312                .await?;
313        }
314
315        Ok(LanguageServerBinary {
316            path: self.node.binary_path().await?,
317            env: None,
318            arguments: eslint_server_binary_arguments(&server_path),
319        })
320    }
321
322    async fn cached_server_binary(
323        &self,
324        container_dir: PathBuf,
325        _: &dyn LspAdapterDelegate,
326    ) -> Option<LanguageServerBinary> {
327        get_cached_eslint_server_binary(container_dir, &*self.node).await
328    }
329
330    async fn installation_test_binary(
331        &self,
332        container_dir: PathBuf,
333    ) -> Option<LanguageServerBinary> {
334        get_cached_eslint_server_binary(container_dir, &*self.node).await
335    }
336
337    async fn label_for_completion(
338        &self,
339        _item: &lsp::CompletionItem,
340        _language: &Arc<language::Language>,
341    ) -> Option<language::CodeLabel> {
342        None
343    }
344
345    fn initialization_options(&self) -> Option<serde_json::Value> {
346        None
347    }
348}
349
350async fn get_cached_eslint_server_binary(
351    container_dir: PathBuf,
352    node: &dyn NodeRuntime,
353) -> Option<LanguageServerBinary> {
354    async_maybe!({
355        // This is unfortunate but we don't know what the version is to build a path directly
356        let mut dir = fs::read_dir(&container_dir).await?;
357        let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
358        if !first.file_type().await?.is_dir() {
359            return Err(anyhow!("First entry is not a directory"));
360        }
361        let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
362
363        Ok(LanguageServerBinary {
364            path: node.binary_path().await?,
365            env: None,
366            arguments: eslint_server_binary_arguments(&server_path),
367        })
368    })
369    .await
370    .log_err()
371}
372
373#[cfg(test)]
374mod tests {
375    use gpui::{Context, TestAppContext};
376    use text::BufferId;
377    use unindent::Unindent;
378
379    #[gpui::test]
380    async fn test_outline(cx: &mut TestAppContext) {
381        let language = crate::language(
382            "typescript",
383            tree_sitter_typescript::language_typescript(),
384            None,
385        )
386        .await;
387
388        let text = r#"
389            function a() {
390              // local variables are omitted
391              let a1 = 1;
392              // all functions are included
393              async function a2() {}
394            }
395            // top-level variables are included
396            let b: C
397            function getB() {}
398            // exported variables are included
399            export const d = e;
400        "#
401        .unindent();
402
403        let buffer = cx.new_model(|cx| {
404            language::Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), text)
405                .with_language(language, cx)
406        });
407        let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
408        assert_eq!(
409            outline
410                .items
411                .iter()
412                .map(|item| (item.text.as_str(), item.depth))
413                .collect::<Vec<_>>(),
414            &[
415                ("function a()", 0),
416                ("async function a2()", 1),
417                ("let b", 0),
418                ("function getB()", 0),
419                ("const d", 0),
420            ]
421        );
422    }
423}