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