typescript.rs

  1use anyhow::{anyhow, Context, Result};
  2use client::http::HttpClient;
  3use futures::{future::BoxFuture, FutureExt, StreamExt};
  4use language::LspAdapter;
  5use serde::Deserialize;
  6use serde_json::json;
  7use smol::fs;
  8use std::{any::Any, path::PathBuf, sync::Arc};
  9use util::{ResultExt, TryFutureExt};
 10
 11pub struct TypeScriptLspAdapter;
 12
 13impl TypeScriptLspAdapter {
 14    const BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
 15}
 16
 17struct Versions {
 18    typescript_version: String,
 19    server_version: String,
 20}
 21
 22impl LspAdapter for TypeScriptLspAdapter {
 23    fn name(&self) -> &'static str {
 24        "typescript-language-server"
 25    }
 26
 27    fn server_args(&self) -> &[&str] {
 28        &["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
 29    }
 30
 31    fn fetch_latest_server_version(
 32        &self,
 33        _: Arc<dyn HttpClient>,
 34    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
 35        async move {
 36            #[derive(Deserialize)]
 37            struct NpmInfo {
 38                versions: Vec<String>,
 39            }
 40
 41            let typescript_output = smol::process::Command::new("npm")
 42                .args(["info", "typescript", "--json"])
 43                .output()
 44                .await?;
 45            if !typescript_output.status.success() {
 46                Err(anyhow!("failed to execute npm info"))?;
 47            }
 48            let mut typescript_info: NpmInfo = serde_json::from_slice(&typescript_output.stdout)?;
 49
 50            let server_output = smol::process::Command::new("npm")
 51                .args(["info", "typescript-language-server", "--json"])
 52                .output()
 53                .await?;
 54            if !server_output.status.success() {
 55                Err(anyhow!("failed to execute npm info"))?;
 56            }
 57            let mut server_info: NpmInfo = serde_json::from_slice(&server_output.stdout)?;
 58
 59            Ok(Box::new(Versions {
 60                typescript_version: typescript_info
 61                    .versions
 62                    .pop()
 63                    .ok_or_else(|| anyhow!("no versions found in typescript npm info"))?,
 64                server_version: server_info.versions.pop().ok_or_else(|| {
 65                    anyhow!("no versions found in typescript language server npm info")
 66                })?,
 67            }) as Box<_>)
 68        }
 69        .boxed()
 70    }
 71
 72    fn fetch_server_binary(
 73        &self,
 74        versions: Box<dyn 'static + Send + Any>,
 75        _: Arc<dyn HttpClient>,
 76        container_dir: PathBuf,
 77    ) -> BoxFuture<'static, Result<PathBuf>> {
 78        let versions = versions.downcast::<Versions>().unwrap();
 79        async move {
 80            let version_dir = container_dir.join(&format!(
 81                "typescript-{}:server-{}",
 82                versions.typescript_version, versions.server_version
 83            ));
 84            fs::create_dir_all(&version_dir)
 85                .await
 86                .context("failed to create version directory")?;
 87            let binary_path = version_dir.join(Self::BIN_PATH);
 88
 89            if fs::metadata(&binary_path).await.is_err() {
 90                let output = smol::process::Command::new("npm")
 91                    .current_dir(&version_dir)
 92                    .arg("install")
 93                    .arg(format!("typescript@{}", versions.typescript_version))
 94                    .arg(format!(
 95                        "typescript-language-server@{}",
 96                        versions.server_version
 97                    ))
 98                    .output()
 99                    .await
100                    .context("failed to run npm install")?;
101                if !output.status.success() {
102                    Err(anyhow!("failed to install typescript-language-server"))?;
103                }
104
105                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
106                    while let Some(entry) = entries.next().await {
107                        if let Some(entry) = entry.log_err() {
108                            let entry_path = entry.path();
109                            if entry_path.as_path() != version_dir {
110                                fs::remove_dir_all(&entry_path).await.log_err();
111                            }
112                        }
113                    }
114                }
115            }
116
117            Ok(binary_path)
118        }
119        .boxed()
120    }
121
122    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
123        async move {
124            let mut last_version_dir = None;
125            let mut entries = fs::read_dir(&container_dir).await?;
126            while let Some(entry) = entries.next().await {
127                let entry = entry?;
128                if entry.file_type().await?.is_dir() {
129                    last_version_dir = Some(entry.path());
130                }
131            }
132            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
133            let bin_path = last_version_dir.join(Self::BIN_PATH);
134            if bin_path.exists() {
135                Ok(bin_path)
136            } else {
137                Err(anyhow!(
138                    "missing executable in directory {:?}",
139                    last_version_dir
140                ))
141            }
142        }
143        .log_err()
144        .boxed()
145    }
146
147    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
148
149    fn initialization_options(&self) -> Option<serde_json::Value> {
150        Some(json!({
151            "provideFormatter": true
152        }))
153    }
154}