1use anyhow::{Context as _, Result};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use async_trait::async_trait;
5use chrono::{DateTime, Local};
6use collections::HashMap;
7use gpui::{App, AppContext, AsyncApp, Task};
8use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
9use language::{
10 ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
11};
12use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
13use node_runtime::NodeRuntime;
14use project::{Fs, lsp_store::language_server_settings};
15use serde_json::{Value, json};
16use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
17use std::{
18 any::Any,
19 borrow::Cow,
20 ffi::OsString,
21 path::{Path, PathBuf},
22 sync::Arc,
23};
24use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
25use util::archive::extract_zip;
26use util::merge_json_value_into;
27use util::{ResultExt, fs::remove_matching, maybe};
28
29pub(crate) struct TypeScriptContextProvider {
30 last_package_json: PackageJsonContents,
31}
32
33const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
34 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
35const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
36 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
37const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =
38 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA"));
39
40const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =
41 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));
42const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =
43 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));
44const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =
45 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUILD_SCRIPT"));
46const TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE: VariableName =
47 VariableName::Custom(Cow::Borrowed("TYPESCRIPT_TEST_SCRIPT"));
48
49#[derive(Clone, Default)]
50struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
51
52struct PackageJson {
53 mtime: DateTime<Local>,
54 data: PackageJsonData,
55}
56
57#[derive(Clone, Copy, Default)]
58struct PackageJsonData {
59 jest: bool,
60 mocha: bool,
61 vitest: bool,
62 jasmine: bool,
63 build_script: bool,
64 test_script: bool,
65 runner: Runner,
66}
67
68#[derive(Clone, Copy, Default)]
69enum Runner {
70 #[default]
71 Npm,
72 Npx,
73 Pnpm,
74}
75
76impl PackageJsonData {
77 fn new(package_json: HashMap<String, Value>) -> Self {
78 let mut build_script = false;
79 let mut test_script = false;
80 if let Some(serde_json::Value::Object(scripts)) = package_json.get("scripts") {
81 build_script |= scripts.contains_key("build");
82 test_script |= scripts.contains_key("test");
83 }
84
85 let mut jest = false;
86 let mut mocha = false;
87 let mut vitest = false;
88 let mut jasmine = false;
89 if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {
90 jest |= dependencies.contains_key("jest");
91 mocha |= dependencies.contains_key("mocha");
92 vitest |= dependencies.contains_key("vitest");
93 jasmine |= dependencies.contains_key("jasmine");
94 }
95 if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")
96 {
97 jest |= dev_dependencies.contains_key("jest");
98 mocha |= dev_dependencies.contains_key("mocha");
99 vitest |= dev_dependencies.contains_key("vitest");
100 jasmine |= dev_dependencies.contains_key("jasmine");
101 }
102
103 let mut runner = Runner::Npm;
104 if which::which("pnpm").is_ok() {
105 runner = Runner::Pnpm;
106 } else if which::which("npx").is_ok() {
107 runner = Runner::Npx;
108 }
109
110 Self {
111 jest,
112 mocha,
113 vitest,
114 jasmine,
115 build_script,
116 test_script,
117 runner,
118 }
119 }
120
121 fn fill_variables(&self, variables: &mut TaskVariables) {
122 let runner = match self.runner {
123 Runner::Npm => "npm",
124 Runner::Npx => "npx",
125 Runner::Pnpm => "pnpm",
126 };
127 variables.insert(TYPESCRIPT_RUNNER_VARIABLE, runner.to_owned());
128
129 if self.jest {
130 variables.insert(TYPESCRIPT_JEST_TASK_VARIABLE, "jest".to_owned());
131 }
132 if self.mocha {
133 variables.insert(TYPESCRIPT_MOCHA_TASK_VARIABLE, "mocha".to_owned());
134 }
135 if self.vitest {
136 variables.insert(TYPESCRIPT_VITEST_TASK_VARIABLE, "vitest".to_owned());
137 }
138 if self.jasmine {
139 variables.insert(TYPESCRIPT_JASMINE_TASK_VARIABLE, "jasmine".to_owned());
140 }
141 if self.build_script {
142 variables.insert(TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE, "build".to_owned());
143 }
144 if self.test_script {
145 variables.insert(TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE, "test".to_owned());
146 }
147 }
148}
149
150impl TypeScriptContextProvider {
151 pub fn new() -> Self {
152 TypeScriptContextProvider {
153 last_package_json: PackageJsonContents::default(),
154 }
155 }
156}
157
158impl ContextProvider for TypeScriptContextProvider {
159 fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Option<TaskTemplates> {
160 let mut task_templates = TaskTemplates(Vec::new());
161
162 // Jest tasks
163 task_templates.0.push(TaskTemplate {
164 label: format!(
165 "{} file test",
166 TYPESCRIPT_JEST_TASK_VARIABLE.template_value()
167 ),
168 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
169 args: vec![
170 TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
171 VariableName::File.template_value(),
172 ],
173 ..TaskTemplate::default()
174 });
175 task_templates.0.push(TaskTemplate {
176 label: format!(
177 "{} test {}",
178 TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
179 VariableName::Symbol.template_value(),
180 ),
181 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
182 args: vec![
183 TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
184 "--testNamePattern".to_owned(),
185 format!("\"{}\"", VariableName::Symbol.template_value()),
186 VariableName::File.template_value(),
187 ],
188 tags: vec![
189 "ts-test".to_owned(),
190 "js-test".to_owned(),
191 "tsx-test".to_owned(),
192 ],
193 ..TaskTemplate::default()
194 });
195
196 // Vitest tasks
197 task_templates.0.push(TaskTemplate {
198 label: format!(
199 "{} file test",
200 TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()
201 ),
202 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
203 args: vec![
204 TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
205 "run".to_owned(),
206 VariableName::File.template_value(),
207 ],
208 ..TaskTemplate::default()
209 });
210 task_templates.0.push(TaskTemplate {
211 label: format!(
212 "{} test {}",
213 TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
214 VariableName::Symbol.template_value(),
215 ),
216 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
217 args: vec![
218 TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
219 "run".to_owned(),
220 "--testNamePattern".to_owned(),
221 format!("\"{}\"", VariableName::Symbol.template_value()),
222 VariableName::File.template_value(),
223 ],
224 tags: vec![
225 "ts-test".to_owned(),
226 "js-test".to_owned(),
227 "tsx-test".to_owned(),
228 ],
229 ..TaskTemplate::default()
230 });
231
232 // Mocha tasks
233 task_templates.0.push(TaskTemplate {
234 label: format!(
235 "{} file test",
236 TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value()
237 ),
238 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
239 args: vec![
240 TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
241 VariableName::File.template_value(),
242 ],
243 ..TaskTemplate::default()
244 });
245 task_templates.0.push(TaskTemplate {
246 label: format!(
247 "{} test {}",
248 TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
249 VariableName::Symbol.template_value(),
250 ),
251 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
252 args: vec![
253 TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
254 "--grep".to_owned(),
255 format!("\"{}\"", VariableName::Symbol.template_value()),
256 VariableName::File.template_value(),
257 ],
258 tags: vec![
259 "ts-test".to_owned(),
260 "js-test".to_owned(),
261 "tsx-test".to_owned(),
262 ],
263 ..TaskTemplate::default()
264 });
265
266 // Jasmine tasks
267 task_templates.0.push(TaskTemplate {
268 label: format!(
269 "{} file test",
270 TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value()
271 ),
272 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
273 args: vec![
274 TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
275 VariableName::File.template_value(),
276 ],
277 ..TaskTemplate::default()
278 });
279 task_templates.0.push(TaskTemplate {
280 label: format!(
281 "{} test {}",
282 TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
283 VariableName::Symbol.template_value(),
284 ),
285 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
286 args: vec![
287 TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
288 format!("--filter={}", VariableName::Symbol.template_value()),
289 VariableName::File.template_value(),
290 ],
291 tags: vec![
292 "ts-test".to_owned(),
293 "js-test".to_owned(),
294 "tsx-test".to_owned(),
295 ],
296 ..TaskTemplate::default()
297 });
298
299 for package_json_script in [
300 TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE,
301 TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE,
302 ] {
303 task_templates.0.push(TaskTemplate {
304 label: format!(
305 "package.json script {}",
306 package_json_script.template_value()
307 ),
308 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
309 args: vec![
310 "--prefix".to_owned(),
311 VariableName::WorktreeRoot.template_value(),
312 "run".to_owned(),
313 package_json_script.template_value(),
314 ],
315 tags: vec!["package-script".into()],
316 ..TaskTemplate::default()
317 });
318 }
319
320 task_templates.0.push(TaskTemplate {
321 label: format!(
322 "execute selection {}",
323 VariableName::SelectedText.template_value()
324 ),
325 command: "node".to_owned(),
326 args: vec![
327 "-e".to_owned(),
328 format!("\"{}\"", VariableName::SelectedText.template_value()),
329 ],
330 ..TaskTemplate::default()
331 });
332
333 Some(task_templates)
334 }
335
336 fn build_context(
337 &self,
338 _variables: &task::TaskVariables,
339 location: ContextLocation<'_>,
340 _project_env: Option<HashMap<String, String>>,
341 _toolchains: Arc<dyn LanguageToolchainStore>,
342 cx: &mut App,
343 ) -> Task<Result<task::TaskVariables>> {
344 let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
345 return Task::ready(Ok(task::TaskVariables::default()));
346 };
347
348 let package_json_contents = self.last_package_json.clone();
349 cx.background_spawn(async move {
350 let variables = package_json_variables(fs, worktree_root, package_json_contents)
351 .await
352 .context("package.json context retrieval")
353 .log_err()
354 .unwrap_or_else(task::TaskVariables::default);
355 Ok(variables)
356 })
357 }
358}
359
360async fn package_json_variables(
361 fs: Arc<dyn Fs>,
362 worktree_root: PathBuf,
363 package_json_contents: PackageJsonContents,
364) -> anyhow::Result<task::TaskVariables> {
365 let package_json_path = worktree_root.join("package.json");
366 let metadata = fs
367 .metadata(&package_json_path)
368 .await
369 .with_context(|| format!("getting metadata for {package_json_path:?}"))?
370 .with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
371 let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
372 let existing_data = {
373 let contents = package_json_contents.0.read().await;
374 contents
375 .get(&package_json_path)
376 .filter(|package_json| package_json.mtime == mtime)
377 .map(|package_json| package_json.data)
378 };
379
380 let mut variables = TaskVariables::default();
381 if let Some(existing_data) = existing_data {
382 existing_data.fill_variables(&mut variables);
383 } else {
384 let package_json_string = fs
385 .load(&package_json_path)
386 .await
387 .with_context(|| format!("loading package.json from {package_json_path:?}"))?;
388 let package_json: HashMap<String, serde_json::Value> =
389 serde_json::from_str(&package_json_string)
390 .with_context(|| format!("parsing package.json from {package_json_path:?}"))?;
391 let new_data = PackageJsonData::new(package_json);
392 new_data.fill_variables(&mut variables);
393 {
394 let mut contents = package_json_contents.0.write().await;
395 contents.insert(
396 package_json_path,
397 PackageJson {
398 mtime,
399 data: new_data,
400 },
401 );
402 }
403 }
404
405 Ok(variables)
406}
407
408fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
409 vec![server_path.into(), "--stdio".into()]
410}
411
412fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
413 vec![
414 "--max-old-space-size=8192".into(),
415 server_path.into(),
416 "--stdio".into(),
417 ]
418}
419
420pub struct TypeScriptLspAdapter {
421 node: NodeRuntime,
422}
423
424impl TypeScriptLspAdapter {
425 const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
426 const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
427 const SERVER_NAME: LanguageServerName =
428 LanguageServerName::new_static("typescript-language-server");
429 const PACKAGE_NAME: &str = "typescript";
430 pub fn new(node: NodeRuntime) -> Self {
431 TypeScriptLspAdapter { node }
432 }
433 async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
434 let is_yarn = adapter
435 .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
436 .await
437 .is_ok();
438
439 let tsdk_path = if is_yarn {
440 ".yarn/sdks/typescript/lib"
441 } else {
442 "node_modules/typescript/lib"
443 };
444
445 if fs
446 .is_dir(&adapter.worktree_root_path().join(tsdk_path))
447 .await
448 {
449 Some(tsdk_path)
450 } else {
451 None
452 }
453 }
454}
455
456struct TypeScriptVersions {
457 typescript_version: String,
458 server_version: String,
459}
460
461#[async_trait(?Send)]
462impl LspAdapter for TypeScriptLspAdapter {
463 fn name(&self) -> LanguageServerName {
464 Self::SERVER_NAME.clone()
465 }
466
467 async fn fetch_latest_server_version(
468 &self,
469 _: &dyn LspAdapterDelegate,
470 ) -> Result<Box<dyn 'static + Send + Any>> {
471 Ok(Box::new(TypeScriptVersions {
472 typescript_version: self.node.npm_package_latest_version("typescript").await?,
473 server_version: self
474 .node
475 .npm_package_latest_version("typescript-language-server")
476 .await?,
477 }) as Box<_>)
478 }
479
480 async fn check_if_version_installed(
481 &self,
482 version: &(dyn 'static + Send + Any),
483 container_dir: &PathBuf,
484 _: &dyn LspAdapterDelegate,
485 ) -> Option<LanguageServerBinary> {
486 let version = version.downcast_ref::<TypeScriptVersions>().unwrap();
487 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
488
489 let should_install_language_server = self
490 .node
491 .should_install_npm_package(
492 Self::PACKAGE_NAME,
493 &server_path,
494 &container_dir,
495 version.typescript_version.as_str(),
496 )
497 .await;
498
499 if should_install_language_server {
500 None
501 } else {
502 Some(LanguageServerBinary {
503 path: self.node.binary_path().await.ok()?,
504 env: None,
505 arguments: typescript_server_binary_arguments(&server_path),
506 })
507 }
508 }
509
510 async fn fetch_server_binary(
511 &self,
512 latest_version: Box<dyn 'static + Send + Any>,
513 container_dir: PathBuf,
514 _: &dyn LspAdapterDelegate,
515 ) -> Result<LanguageServerBinary> {
516 let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
517 let server_path = container_dir.join(Self::NEW_SERVER_PATH);
518
519 self.node
520 .npm_install_packages(
521 &container_dir,
522 &[
523 (
524 Self::PACKAGE_NAME,
525 latest_version.typescript_version.as_str(),
526 ),
527 (
528 "typescript-language-server",
529 latest_version.server_version.as_str(),
530 ),
531 ],
532 )
533 .await?;
534
535 Ok(LanguageServerBinary {
536 path: self.node.binary_path().await?,
537 env: None,
538 arguments: typescript_server_binary_arguments(&server_path),
539 })
540 }
541
542 async fn cached_server_binary(
543 &self,
544 container_dir: PathBuf,
545 _: &dyn LspAdapterDelegate,
546 ) -> Option<LanguageServerBinary> {
547 get_cached_ts_server_binary(container_dir, &self.node).await
548 }
549
550 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
551 Some(vec![
552 CodeActionKind::QUICKFIX,
553 CodeActionKind::REFACTOR,
554 CodeActionKind::REFACTOR_EXTRACT,
555 CodeActionKind::SOURCE,
556 ])
557 }
558
559 async fn label_for_completion(
560 &self,
561 item: &lsp::CompletionItem,
562 language: &Arc<language::Language>,
563 ) -> Option<language::CodeLabel> {
564 use lsp::CompletionItemKind as Kind;
565 let len = item.label.len();
566 let grammar = language.grammar()?;
567 let highlight_id = match item.kind? {
568 Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
569 Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
570 Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
571 Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
572 Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
573 Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
574 _ => None,
575 }?;
576
577 let text = if let Some(description) = item
578 .label_details
579 .as_ref()
580 .and_then(|label_details| label_details.description.as_ref())
581 {
582 format!("{} {}", item.label, description)
583 } else if let Some(detail) = &item.detail {
584 format!("{} {}", item.label, detail)
585 } else {
586 item.label.clone()
587 };
588
589 Some(language::CodeLabel {
590 text,
591 runs: vec![(0..len, highlight_id)],
592 filter_range: 0..len,
593 })
594 }
595
596 async fn initialization_options(
597 self: Arc<Self>,
598 fs: &dyn Fs,
599 adapter: &Arc<dyn LspAdapterDelegate>,
600 ) -> Result<Option<serde_json::Value>> {
601 let tsdk_path = Self::tsdk_path(fs, adapter).await;
602 Ok(Some(json!({
603 "provideFormatter": true,
604 "hostInfo": "zed",
605 "tsserver": {
606 "path": tsdk_path,
607 },
608 "preferences": {
609 "includeInlayParameterNameHints": "all",
610 "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
611 "includeInlayFunctionParameterTypeHints": true,
612 "includeInlayVariableTypeHints": true,
613 "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
614 "includeInlayPropertyDeclarationTypeHints": true,
615 "includeInlayFunctionLikeReturnTypeHints": true,
616 "includeInlayEnumMemberValueHints": true,
617 }
618 })))
619 }
620
621 async fn workspace_configuration(
622 self: Arc<Self>,
623 _: &dyn Fs,
624 delegate: &Arc<dyn LspAdapterDelegate>,
625 _: Arc<dyn LanguageToolchainStore>,
626 cx: &mut AsyncApp,
627 ) -> Result<Value> {
628 let override_options = cx.update(|cx| {
629 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
630 .and_then(|s| s.settings.clone())
631 })?;
632 if let Some(options) = override_options {
633 return Ok(options);
634 }
635 Ok(json!({
636 "completions": {
637 "completeFunctionCalls": true
638 }
639 }))
640 }
641
642 fn language_ids(&self) -> HashMap<String, String> {
643 HashMap::from_iter([
644 ("TypeScript".into(), "typescript".into()),
645 ("JavaScript".into(), "javascript".into()),
646 ("TSX".into(), "typescriptreact".into()),
647 ])
648 }
649}
650
651async fn get_cached_ts_server_binary(
652 container_dir: PathBuf,
653 node: &NodeRuntime,
654) -> Option<LanguageServerBinary> {
655 maybe!(async {
656 let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
657 let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
658 if new_server_path.exists() {
659 Ok(LanguageServerBinary {
660 path: node.binary_path().await?,
661 env: None,
662 arguments: typescript_server_binary_arguments(&new_server_path),
663 })
664 } else if old_server_path.exists() {
665 Ok(LanguageServerBinary {
666 path: node.binary_path().await?,
667 env: None,
668 arguments: typescript_server_binary_arguments(&old_server_path),
669 })
670 } else {
671 anyhow::bail!("missing executable in directory {container_dir:?}")
672 }
673 })
674 .await
675 .log_err()
676}
677
678pub struct EsLintLspAdapter {
679 node: NodeRuntime,
680}
681
682impl EsLintLspAdapter {
683 const CURRENT_VERSION: &'static str = "2.4.4";
684 const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
685
686 #[cfg(not(windows))]
687 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
688 #[cfg(windows)]
689 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
690
691 const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
692 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
693
694 const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
695 "eslint.config.js",
696 "eslint.config.mjs",
697 "eslint.config.cjs",
698 "eslint.config.ts",
699 "eslint.config.cts",
700 "eslint.config.mts",
701 ];
702
703 pub fn new(node: NodeRuntime) -> Self {
704 EsLintLspAdapter { node }
705 }
706
707 fn build_destination_path(container_dir: &Path) -> PathBuf {
708 container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
709 }
710}
711
712#[async_trait(?Send)]
713impl LspAdapter for EsLintLspAdapter {
714 fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
715 Some(vec![
716 CodeActionKind::QUICKFIX,
717 CodeActionKind::new("source.fixAll.eslint"),
718 ])
719 }
720
721 async fn workspace_configuration(
722 self: Arc<Self>,
723 _: &dyn Fs,
724 delegate: &Arc<dyn LspAdapterDelegate>,
725 _: Arc<dyn LanguageToolchainStore>,
726 cx: &mut AsyncApp,
727 ) -> Result<Value> {
728 let workspace_root = delegate.worktree_root_path();
729 let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
730 .iter()
731 .any(|file| workspace_root.join(file).is_file());
732
733 let mut default_workspace_configuration = json!({
734 "validate": "on",
735 "rulesCustomizations": [],
736 "run": "onType",
737 "nodePath": null,
738 "workingDirectory": {
739 "mode": "auto"
740 },
741 "workspaceFolder": {
742 "uri": workspace_root,
743 "name": workspace_root.file_name()
744 .unwrap_or(workspace_root.as_os_str())
745 .to_string_lossy(),
746 },
747 "problems": {},
748 "codeActionOnSave": {
749 // We enable this, but without also configuring code_actions_on_format
750 // in the Zed configuration, it doesn't have an effect.
751 "enable": true,
752 },
753 "codeAction": {
754 "disableRuleComment": {
755 "enable": true,
756 "location": "separateLine",
757 },
758 "showDocumentation": {
759 "enable": true
760 }
761 },
762 "experimental": {
763 "useFlatConfig": use_flat_config,
764 },
765 });
766
767 let override_options = cx.update(|cx| {
768 language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
769 .and_then(|s| s.settings.clone())
770 })?;
771
772 if let Some(override_options) = override_options {
773 merge_json_value_into(override_options, &mut default_workspace_configuration);
774 }
775
776 Ok(json!({
777 "": default_workspace_configuration
778 }))
779 }
780
781 fn name(&self) -> LanguageServerName {
782 Self::SERVER_NAME.clone()
783 }
784
785 async fn fetch_latest_server_version(
786 &self,
787 _delegate: &dyn LspAdapterDelegate,
788 ) -> Result<Box<dyn 'static + Send + Any>> {
789 let url = build_asset_url(
790 "zed-industries/vscode-eslint",
791 Self::CURRENT_VERSION_TAG_NAME,
792 Self::GITHUB_ASSET_KIND,
793 )?;
794
795 Ok(Box::new(GitHubLspBinaryVersion {
796 name: Self::CURRENT_VERSION.into(),
797 url,
798 }))
799 }
800
801 async fn fetch_server_binary(
802 &self,
803 version: Box<dyn 'static + Send + Any>,
804 container_dir: PathBuf,
805 delegate: &dyn LspAdapterDelegate,
806 ) -> Result<LanguageServerBinary> {
807 let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
808 let destination_path = Self::build_destination_path(&container_dir);
809 let server_path = destination_path.join(Self::SERVER_PATH);
810
811 if fs::metadata(&server_path).await.is_err() {
812 remove_matching(&container_dir, |entry| entry != destination_path).await;
813
814 let mut response = delegate
815 .http_client()
816 .get(&version.url, Default::default(), true)
817 .await
818 .context("downloading release")?;
819 match Self::GITHUB_ASSET_KIND {
820 AssetKind::TarGz => {
821 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
822 let archive = Archive::new(decompressed_bytes);
823 archive.unpack(&destination_path).await.with_context(|| {
824 format!("extracting {} to {:?}", version.url, destination_path)
825 })?;
826 }
827 AssetKind::Gz => {
828 let mut decompressed_bytes =
829 GzipDecoder::new(BufReader::new(response.body_mut()));
830 let mut file =
831 fs::File::create(&destination_path).await.with_context(|| {
832 format!(
833 "creating a file {:?} for a download from {}",
834 destination_path, version.url,
835 )
836 })?;
837 futures::io::copy(&mut decompressed_bytes, &mut file)
838 .await
839 .with_context(|| {
840 format!("extracting {} to {:?}", version.url, destination_path)
841 })?;
842 }
843 AssetKind::Zip => {
844 extract_zip(&destination_path, response.body_mut())
845 .await
846 .with_context(|| {
847 format!("unzipping {} to {:?}", version.url, destination_path)
848 })?;
849 }
850 }
851
852 let mut dir = fs::read_dir(&destination_path).await?;
853 let first = dir.next().await.context("missing first file")??;
854 let repo_root = destination_path.join("vscode-eslint");
855 fs::rename(first.path(), &repo_root).await?;
856
857 #[cfg(target_os = "windows")]
858 {
859 handle_symlink(
860 repo_root.join("$shared"),
861 repo_root.join("client").join("src").join("shared"),
862 )
863 .await?;
864 handle_symlink(
865 repo_root.join("$shared"),
866 repo_root.join("server").join("src").join("shared"),
867 )
868 .await?;
869 }
870
871 self.node
872 .run_npm_subcommand(&repo_root, "install", &[])
873 .await?;
874
875 self.node
876 .run_npm_subcommand(&repo_root, "run-script", &["compile"])
877 .await?;
878 }
879
880 Ok(LanguageServerBinary {
881 path: self.node.binary_path().await?,
882 env: None,
883 arguments: eslint_server_binary_arguments(&server_path),
884 })
885 }
886
887 async fn cached_server_binary(
888 &self,
889 container_dir: PathBuf,
890 _: &dyn LspAdapterDelegate,
891 ) -> Option<LanguageServerBinary> {
892 let server_path =
893 Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
894 Some(LanguageServerBinary {
895 path: self.node.binary_path().await.ok()?,
896 env: None,
897 arguments: eslint_server_binary_arguments(&server_path),
898 })
899 }
900}
901
902#[cfg(target_os = "windows")]
903async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
904 anyhow::ensure!(
905 fs::metadata(&src_dir).await.is_ok(),
906 "Directory {src_dir:?} is not present"
907 );
908 if fs::metadata(&dest_dir).await.is_ok() {
909 fs::remove_file(&dest_dir).await?;
910 }
911 fs::create_dir_all(&dest_dir).await?;
912 let mut entries = fs::read_dir(&src_dir).await?;
913 while let Some(entry) = entries.try_next().await? {
914 let entry_path = entry.path();
915 let entry_name = entry.file_name();
916 let dest_path = dest_dir.join(&entry_name);
917 fs::copy(&entry_path, &dest_path).await?;
918 }
919 Ok(())
920}
921
922#[cfg(test)]
923mod tests {
924 use gpui::{AppContext as _, TestAppContext};
925 use unindent::Unindent;
926
927 #[gpui::test]
928 async fn test_outline(cx: &mut TestAppContext) {
929 let language = crate::language(
930 "typescript",
931 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
932 );
933
934 let text = r#"
935 function a() {
936 // local variables are omitted
937 let a1 = 1;
938 // all functions are included
939 async function a2() {}
940 }
941 // top-level variables are included
942 let b: C
943 function getB() {}
944 // exported variables are included
945 export const d = e;
946 "#
947 .unindent();
948
949 let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
950 let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
951 assert_eq!(
952 outline
953 .items
954 .iter()
955 .map(|item| (item.text.as_str(), item.depth))
956 .collect::<Vec<_>>(),
957 &[
958 ("function a()", 0),
959 ("async function a2()", 1),
960 ("let b", 0),
961 ("function getB()", 0),
962 ("const d", 0),
963 ]
964 );
965 }
966}