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}