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