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