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