typescript.rs

   1use anyhow::{Context as _, Result};
   2use async_trait::async_trait;
   3use chrono::{DateTime, Local};
   4use collections::HashMap;
   5use futures::future::join_all;
   6use gpui::{App, AppContext, AsyncApp, Task};
   7use itertools::Itertools as _;
   8use language::{
   9    ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
  10    LspAdapterDelegate, LspInstaller, Toolchain,
  11};
  12use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
  13use node_runtime::{NodeRuntime, VersionStrategy};
  14use project::{Fs, lsp_store::language_server_settings};
  15use serde_json::{Value, json};
  16use smol::lock::RwLock;
  17use std::{
  18    borrow::Cow,
  19    ffi::OsString,
  20    path::{Path, PathBuf},
  21    sync::{Arc, LazyLock},
  22};
  23use task::{TaskTemplate, TaskTemplates, VariableName};
  24use util::rel_path::RelPath;
  25use util::{ResultExt, maybe};
  26
  27use crate::{PackageJson, PackageJsonData};
  28
  29pub(crate) struct TypeScriptContextProvider {
  30    fs: Arc<dyn Fs>,
  31    last_package_json: PackageJsonContents,
  32}
  33
  34const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
  35    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
  36
  37const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
  38    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
  39
  40const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
  41    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
  42
  43const TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE: VariableName =
  44    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_PACKAGE_PATH"));
  45
  46const TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE: VariableName =
  47    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA_PACKAGE_PATH"));
  48
  49const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
  50    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_PACKAGE_PATH"));
  51
  52const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
  53    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
  54
  55const TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE: VariableName =
  56    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUN_PACKAGE_PATH"));
  57
  58const TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE: VariableName =
  59    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_NODE_PACKAGE_PATH"));
  60
  61#[derive(Clone, Debug, Default)]
  62struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
  63
  64impl PackageJsonData {
  65    fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
  66        if self.jest_package_path.is_some() {
  67            task_templates.0.push(TaskTemplate {
  68                label: "jest file test".to_owned(),
  69                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
  70                args: vec![
  71                    "exec".to_owned(),
  72                    "--".to_owned(),
  73                    "jest".to_owned(),
  74                    "--runInBand".to_owned(),
  75                    VariableName::File.template_value(),
  76                ],
  77                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
  78                ..TaskTemplate::default()
  79            });
  80            task_templates.0.push(TaskTemplate {
  81                label: format!("jest test {}", VariableName::Symbol.template_value()),
  82                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
  83                args: vec![
  84                    "exec".to_owned(),
  85                    "--".to_owned(),
  86                    "jest".to_owned(),
  87                    "--runInBand".to_owned(),
  88                    "--testNamePattern".to_owned(),
  89                    format!(
  90                        "\"{}\"",
  91                        TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
  92                    ),
  93                    VariableName::File.template_value(),
  94                ],
  95                tags: vec![
  96                    "ts-test".to_owned(),
  97                    "js-test".to_owned(),
  98                    "tsx-test".to_owned(),
  99                ],
 100                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
 101                ..TaskTemplate::default()
 102            });
 103        }
 104
 105        if self.vitest_package_path.is_some() {
 106            task_templates.0.push(TaskTemplate {
 107                label: format!("{} file test", "vitest".to_owned()),
 108                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 109                args: vec![
 110                    "exec".to_owned(),
 111                    "--".to_owned(),
 112                    "vitest".to_owned(),
 113                    "run".to_owned(),
 114                    "--poolOptions.forks.minForks=0".to_owned(),
 115                    "--poolOptions.forks.maxForks=1".to_owned(),
 116                    VariableName::File.template_value(),
 117                ],
 118                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
 119                ..TaskTemplate::default()
 120            });
 121            task_templates.0.push(TaskTemplate {
 122                label: format!(
 123                    "{} test {}",
 124                    "vitest".to_owned(),
 125                    VariableName::Symbol.template_value(),
 126                ),
 127                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 128                args: vec![
 129                    "exec".to_owned(),
 130                    "--".to_owned(),
 131                    "vitest".to_owned(),
 132                    "run".to_owned(),
 133                    "--poolOptions.forks.minForks=0".to_owned(),
 134                    "--poolOptions.forks.maxForks=1".to_owned(),
 135                    "--testNamePattern".to_owned(),
 136                    format!(
 137                        "\"{}\"",
 138                        TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()
 139                    ),
 140                    VariableName::File.template_value(),
 141                ],
 142                tags: vec![
 143                    "ts-test".to_owned(),
 144                    "js-test".to_owned(),
 145                    "tsx-test".to_owned(),
 146                ],
 147                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
 148                ..TaskTemplate::default()
 149            });
 150        }
 151
 152        if self.mocha_package_path.is_some() {
 153            task_templates.0.push(TaskTemplate {
 154                label: format!("{} file test", "mocha".to_owned()),
 155                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 156                args: vec![
 157                    "exec".to_owned(),
 158                    "--".to_owned(),
 159                    "mocha".to_owned(),
 160                    VariableName::File.template_value(),
 161                ],
 162                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
 163                ..TaskTemplate::default()
 164            });
 165            task_templates.0.push(TaskTemplate {
 166                label: format!(
 167                    "{} test {}",
 168                    "mocha".to_owned(),
 169                    VariableName::Symbol.template_value(),
 170                ),
 171                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 172                args: vec![
 173                    "exec".to_owned(),
 174                    "--".to_owned(),
 175                    "mocha".to_owned(),
 176                    "--grep".to_owned(),
 177                    format!("\"{}\"", VariableName::Symbol.template_value()),
 178                    VariableName::File.template_value(),
 179                ],
 180                tags: vec![
 181                    "ts-test".to_owned(),
 182                    "js-test".to_owned(),
 183                    "tsx-test".to_owned(),
 184                ],
 185                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
 186                ..TaskTemplate::default()
 187            });
 188        }
 189
 190        if self.jasmine_package_path.is_some() {
 191            task_templates.0.push(TaskTemplate {
 192                label: format!("{} file test", "jasmine".to_owned()),
 193                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 194                args: vec![
 195                    "exec".to_owned(),
 196                    "--".to_owned(),
 197                    "jasmine".to_owned(),
 198                    VariableName::File.template_value(),
 199                ],
 200                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
 201                ..TaskTemplate::default()
 202            });
 203            task_templates.0.push(TaskTemplate {
 204                label: format!(
 205                    "{} test {}",
 206                    "jasmine".to_owned(),
 207                    VariableName::Symbol.template_value(),
 208                ),
 209                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 210                args: vec![
 211                    "exec".to_owned(),
 212                    "--".to_owned(),
 213                    "jasmine".to_owned(),
 214                    format!("--filter={}", VariableName::Symbol.template_value()),
 215                    VariableName::File.template_value(),
 216                ],
 217                tags: vec![
 218                    "ts-test".to_owned(),
 219                    "js-test".to_owned(),
 220                    "tsx-test".to_owned(),
 221                ],
 222                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
 223                ..TaskTemplate::default()
 224            });
 225        }
 226
 227        if self.bun_package_path.is_some() {
 228            task_templates.0.push(TaskTemplate {
 229                label: format!("{} file test", "bun test".to_owned()),
 230                command: "bun".to_owned(),
 231                args: vec!["test".to_owned(), VariableName::File.template_value()],
 232                cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
 233                ..TaskTemplate::default()
 234            });
 235            task_templates.0.push(TaskTemplate {
 236                label: format!("bun test {}", VariableName::Symbol.template_value(),),
 237                command: "bun".to_owned(),
 238                args: vec![
 239                    "test".to_owned(),
 240                    "--test-name-pattern".to_owned(),
 241                    format!("\"{}\"", VariableName::Symbol.template_value()),
 242                    VariableName::File.template_value(),
 243                ],
 244                tags: vec![
 245                    "ts-test".to_owned(),
 246                    "js-test".to_owned(),
 247                    "tsx-test".to_owned(),
 248                ],
 249                cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
 250                ..TaskTemplate::default()
 251            });
 252        }
 253
 254        if self.node_package_path.is_some() {
 255            task_templates.0.push(TaskTemplate {
 256                label: format!("{} file test", "node test".to_owned()),
 257                command: "node".to_owned(),
 258                args: vec!["--test".to_owned(), VariableName::File.template_value()],
 259                tags: vec![
 260                    "ts-test".to_owned(),
 261                    "js-test".to_owned(),
 262                    "tsx-test".to_owned(),
 263                ],
 264                cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
 265                ..TaskTemplate::default()
 266            });
 267            task_templates.0.push(TaskTemplate {
 268                label: format!("node test {}", VariableName::Symbol.template_value()),
 269                command: "node".to_owned(),
 270                args: vec![
 271                    "--test".to_owned(),
 272                    "--test-name-pattern".to_owned(),
 273                    format!("\"{}\"", VariableName::Symbol.template_value()),
 274                    VariableName::File.template_value(),
 275                ],
 276                tags: vec![
 277                    "ts-test".to_owned(),
 278                    "js-test".to_owned(),
 279                    "tsx-test".to_owned(),
 280                ],
 281                cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
 282                ..TaskTemplate::default()
 283            });
 284        }
 285
 286        let script_name_counts: HashMap<_, usize> =
 287            self.scripts
 288                .iter()
 289                .fold(HashMap::default(), |mut acc, (_, script)| {
 290                    *acc.entry(script).or_default() += 1;
 291                    acc
 292                });
 293        for (path, script) in &self.scripts {
 294            let label = if script_name_counts.get(script).copied().unwrap_or_default() > 1
 295                && let Some(parent) = path.parent().and_then(|parent| parent.file_name())
 296            {
 297                let parent = parent.to_string_lossy();
 298                format!("{parent}/package.json > {script}")
 299            } else {
 300                format!("package.json > {script}")
 301            };
 302            task_templates.0.push(TaskTemplate {
 303                label,
 304                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 305                args: vec!["run".to_owned(), script.to_owned()],
 306                tags: vec!["package-script".into()],
 307                cwd: Some(
 308                    path.parent()
 309                        .unwrap_or(Path::new("/"))
 310                        .to_string_lossy()
 311                        .to_string(),
 312                ),
 313                ..TaskTemplate::default()
 314            });
 315        }
 316    }
 317}
 318
 319impl TypeScriptContextProvider {
 320    pub fn new(fs: Arc<dyn Fs>) -> Self {
 321        Self {
 322            fs,
 323            last_package_json: PackageJsonContents::default(),
 324        }
 325    }
 326
 327    fn combined_package_json_data(
 328        &self,
 329        fs: Arc<dyn Fs>,
 330        worktree_root: &Path,
 331        file_relative_path: &RelPath,
 332        cx: &App,
 333    ) -> Task<anyhow::Result<PackageJsonData>> {
 334        let new_json_data = file_relative_path
 335            .ancestors()
 336            .map(|path| worktree_root.join(path.as_std_path()))
 337            .map(|parent_path| {
 338                self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)
 339            })
 340            .collect::<Vec<_>>();
 341
 342        cx.background_spawn(async move {
 343            let mut package_json_data = PackageJsonData::default();
 344            for new_data in join_all(new_json_data).await.into_iter().flatten() {
 345                package_json_data.merge(new_data);
 346            }
 347            Ok(package_json_data)
 348        })
 349    }
 350
 351    fn package_json_data(
 352        &self,
 353        directory_path: &Path,
 354        existing_package_json: PackageJsonContents,
 355        fs: Arc<dyn Fs>,
 356        cx: &App,
 357    ) -> Task<anyhow::Result<PackageJsonData>> {
 358        let package_json_path = directory_path.join("package.json");
 359        let metadata_check_fs = fs.clone();
 360        cx.background_spawn(async move {
 361            let metadata = metadata_check_fs
 362                .metadata(&package_json_path)
 363                .await
 364                .with_context(|| format!("getting metadata for {package_json_path:?}"))?
 365                .with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
 366            let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
 367            let existing_data = {
 368                let contents = existing_package_json.0.read().await;
 369                contents
 370                    .get(&package_json_path)
 371                    .filter(|package_json| package_json.mtime == mtime)
 372                    .map(|package_json| package_json.data.clone())
 373            };
 374            match existing_data {
 375                Some(existing_data) => Ok(existing_data),
 376                None => {
 377                    let package_json_string =
 378                        fs.load(&package_json_path).await.with_context(|| {
 379                            format!("loading package.json from {package_json_path:?}")
 380                        })?;
 381                    let package_json: HashMap<String, serde_json_lenient::Value> =
 382                        serde_json_lenient::from_str(&package_json_string).with_context(|| {
 383                            format!("parsing package.json from {package_json_path:?}")
 384                        })?;
 385                    let new_data =
 386                        PackageJsonData::new(package_json_path.as_path().into(), package_json);
 387                    {
 388                        let mut contents = existing_package_json.0.write().await;
 389                        contents.insert(
 390                            package_json_path,
 391                            PackageJson {
 392                                mtime,
 393                                data: new_data.clone(),
 394                            },
 395                        );
 396                    }
 397                    Ok(new_data)
 398                }
 399            }
 400        })
 401    }
 402}
 403
 404async fn detect_package_manager(
 405    worktree_root: PathBuf,
 406    fs: Arc<dyn Fs>,
 407    package_json_data: Option<PackageJsonData>,
 408) -> &'static str {
 409    if let Some(package_json_data) = package_json_data
 410        && let Some(package_manager) = package_json_data.package_manager
 411    {
 412        return package_manager;
 413    }
 414    if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
 415        return "pnpm";
 416    }
 417    if fs.is_file(&worktree_root.join("yarn.lock")).await {
 418        return "yarn";
 419    }
 420    "npm"
 421}
 422
 423impl ContextProvider for TypeScriptContextProvider {
 424    fn associated_tasks(
 425        &self,
 426        file: Option<Arc<dyn File>>,
 427        cx: &App,
 428    ) -> Task<Option<TaskTemplates>> {
 429        let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
 430            return Task::ready(None);
 431        };
 432        let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
 433            return Task::ready(None);
 434        };
 435        let file_relative_path = file.path().clone();
 436        let package_json_data = self.combined_package_json_data(
 437            self.fs.clone(),
 438            &worktree_root,
 439            &file_relative_path,
 440            cx,
 441        );
 442
 443        cx.background_spawn(async move {
 444            let mut task_templates = TaskTemplates(Vec::new());
 445            task_templates.0.push(TaskTemplate {
 446                label: format!(
 447                    "execute selection {}",
 448                    VariableName::SelectedText.template_value()
 449                ),
 450                command: "node".to_owned(),
 451                args: vec![
 452                    "-e".to_owned(),
 453                    format!("\"{}\"", VariableName::SelectedText.template_value()),
 454                ],
 455                ..TaskTemplate::default()
 456            });
 457
 458            match package_json_data.await {
 459                Ok(package_json) => {
 460                    package_json.fill_task_templates(&mut task_templates);
 461                }
 462                Err(e) => {
 463                    log::error!(
 464                        "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
 465                    );
 466                }
 467            }
 468
 469            Some(task_templates)
 470        })
 471    }
 472
 473    fn build_context(
 474        &self,
 475        current_vars: &task::TaskVariables,
 476        location: ContextLocation<'_>,
 477        _project_env: Option<HashMap<String, String>>,
 478        _toolchains: Arc<dyn LanguageToolchainStore>,
 479        cx: &mut App,
 480    ) -> Task<Result<task::TaskVariables>> {
 481        let mut vars = task::TaskVariables::default();
 482
 483        if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
 484            vars.insert(
 485                TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
 486                replace_test_name_parameters(symbol),
 487            );
 488            vars.insert(
 489                TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
 490                replace_test_name_parameters(symbol),
 491            );
 492        }
 493        let file_path = location
 494            .file_location
 495            .buffer
 496            .read(cx)
 497            .file()
 498            .map(|file| file.path());
 499
 500        let args = location.worktree_root.zip(location.fs).zip(file_path).map(
 501            |((worktree_root, fs), file_path)| {
 502                (
 503                    self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
 504                    worktree_root,
 505                    fs,
 506                )
 507            },
 508        );
 509        cx.background_spawn(async move {
 510            if let Some((task, worktree_root, fs)) = args {
 511                let package_json_data = task.await.log_err();
 512                vars.insert(
 513                    TYPESCRIPT_RUNNER_VARIABLE,
 514                    detect_package_manager(worktree_root, fs, package_json_data.clone())
 515                        .await
 516                        .to_owned(),
 517                );
 518
 519                if let Some(package_json_data) = package_json_data {
 520                    if let Some(path) = package_json_data.jest_package_path {
 521                        vars.insert(
 522                            TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
 523                            path.parent()
 524                                .unwrap_or(Path::new(""))
 525                                .to_string_lossy()
 526                                .to_string(),
 527                        );
 528                    }
 529
 530                    if let Some(path) = package_json_data.mocha_package_path {
 531                        vars.insert(
 532                            TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
 533                            path.parent()
 534                                .unwrap_or(Path::new(""))
 535                                .to_string_lossy()
 536                                .to_string(),
 537                        );
 538                    }
 539
 540                    if let Some(path) = package_json_data.vitest_package_path {
 541                        vars.insert(
 542                            TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
 543                            path.parent()
 544                                .unwrap_or(Path::new(""))
 545                                .to_string_lossy()
 546                                .to_string(),
 547                        );
 548                    }
 549
 550                    if let Some(path) = package_json_data.jasmine_package_path {
 551                        vars.insert(
 552                            TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
 553                            path.parent()
 554                                .unwrap_or(Path::new(""))
 555                                .to_string_lossy()
 556                                .to_string(),
 557                        );
 558                    }
 559
 560                    if let Some(path) = package_json_data.bun_package_path {
 561                        vars.insert(
 562                            TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE,
 563                            path.parent()
 564                                .unwrap_or(Path::new(""))
 565                                .to_string_lossy()
 566                                .to_string(),
 567                        );
 568                    }
 569
 570                    if let Some(path) = package_json_data.node_package_path {
 571                        vars.insert(
 572                            TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE,
 573                            path.parent()
 574                                .unwrap_or(Path::new(""))
 575                                .to_string_lossy()
 576                                .to_string(),
 577                        );
 578                    }
 579                }
 580            }
 581            Ok(vars)
 582        })
 583    }
 584}
 585
 586fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 587    vec![server_path.into(), "--stdio".into()]
 588}
 589
 590fn replace_test_name_parameters(test_name: &str) -> String {
 591    static PATTERN: LazyLock<regex::Regex> =
 592        LazyLock::new(|| regex::Regex::new(r"(\$([A-Za-z0-9_\.]+|[\#])|%[psdifjo#\$%])").unwrap());
 593    PATTERN.split(test_name).map(regex::escape).join("(.+?)")
 594}
 595
 596pub struct TypeScriptLspAdapter {
 597    fs: Arc<dyn Fs>,
 598    node: NodeRuntime,
 599}
 600
 601impl TypeScriptLspAdapter {
 602    const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
 603    const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
 604    const SERVER_NAME: LanguageServerName =
 605        LanguageServerName::new_static("typescript-language-server");
 606    const PACKAGE_NAME: &str = "typescript";
 607    pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
 608        TypeScriptLspAdapter { fs, node }
 609    }
 610    async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
 611        let is_yarn = adapter
 612            .read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap())
 613            .await
 614            .is_ok();
 615
 616        let tsdk_path = if is_yarn {
 617            ".yarn/sdks/typescript/lib"
 618        } else {
 619            "node_modules/typescript/lib"
 620        };
 621
 622        if self
 623            .fs
 624            .is_dir(&adapter.worktree_root_path().join(tsdk_path))
 625            .await
 626        {
 627            Some(tsdk_path)
 628        } else {
 629            None
 630        }
 631    }
 632}
 633
 634pub struct TypeScriptVersions {
 635    typescript_version: String,
 636    server_version: String,
 637}
 638
 639impl LspInstaller for TypeScriptLspAdapter {
 640    type BinaryVersion = TypeScriptVersions;
 641
 642    async fn fetch_latest_server_version(
 643        &self,
 644        _: &dyn LspAdapterDelegate,
 645        _: bool,
 646        _: &mut AsyncApp,
 647    ) -> Result<TypeScriptVersions> {
 648        Ok(TypeScriptVersions {
 649            typescript_version: self.node.npm_package_latest_version("typescript").await?,
 650            server_version: self
 651                .node
 652                .npm_package_latest_version("typescript-language-server")
 653                .await?,
 654        })
 655    }
 656
 657    async fn check_if_version_installed(
 658        &self,
 659        version: &TypeScriptVersions,
 660        container_dir: &PathBuf,
 661        _: &dyn LspAdapterDelegate,
 662    ) -> Option<LanguageServerBinary> {
 663        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 664
 665        let should_install_language_server = self
 666            .node
 667            .should_install_npm_package(
 668                Self::PACKAGE_NAME,
 669                &server_path,
 670                container_dir,
 671                VersionStrategy::Latest(version.typescript_version.as_str()),
 672            )
 673            .await;
 674
 675        if should_install_language_server {
 676            None
 677        } else {
 678            Some(LanguageServerBinary {
 679                path: self.node.binary_path().await.ok()?,
 680                env: None,
 681                arguments: typescript_server_binary_arguments(&server_path),
 682            })
 683        }
 684    }
 685
 686    async fn fetch_server_binary(
 687        &self,
 688        latest_version: TypeScriptVersions,
 689        container_dir: PathBuf,
 690        _: &dyn LspAdapterDelegate,
 691    ) -> Result<LanguageServerBinary> {
 692        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 693
 694        self.node
 695            .npm_install_packages(
 696                &container_dir,
 697                &[
 698                    (
 699                        Self::PACKAGE_NAME,
 700                        latest_version.typescript_version.as_str(),
 701                    ),
 702                    (
 703                        "typescript-language-server",
 704                        latest_version.server_version.as_str(),
 705                    ),
 706                ],
 707            )
 708            .await?;
 709
 710        Ok(LanguageServerBinary {
 711            path: self.node.binary_path().await?,
 712            env: None,
 713            arguments: typescript_server_binary_arguments(&server_path),
 714        })
 715    }
 716
 717    async fn cached_server_binary(
 718        &self,
 719        container_dir: PathBuf,
 720        _: &dyn LspAdapterDelegate,
 721    ) -> Option<LanguageServerBinary> {
 722        get_cached_ts_server_binary(container_dir, &self.node).await
 723    }
 724}
 725
 726#[async_trait(?Send)]
 727impl LspAdapter for TypeScriptLspAdapter {
 728    fn name(&self) -> LanguageServerName {
 729        Self::SERVER_NAME
 730    }
 731
 732    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
 733        Some(vec![
 734            CodeActionKind::QUICKFIX,
 735            CodeActionKind::REFACTOR,
 736            CodeActionKind::REFACTOR_EXTRACT,
 737            CodeActionKind::SOURCE,
 738        ])
 739    }
 740
 741    async fn label_for_completion(
 742        &self,
 743        item: &lsp::CompletionItem,
 744        language: &Arc<language::Language>,
 745    ) -> Option<language::CodeLabel> {
 746        use lsp::CompletionItemKind as Kind;
 747        let label_len = item.label.len();
 748        let grammar = language.grammar()?;
 749        let highlight_id = match item.kind? {
 750            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
 751            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
 752            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
 753            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
 754            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
 755            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
 756            _ => None,
 757        }?;
 758
 759        let text = if let Some(description) = item
 760            .label_details
 761            .as_ref()
 762            .and_then(|label_details| label_details.description.as_ref())
 763        {
 764            format!("{} {}", item.label, description)
 765        } else if let Some(detail) = &item.detail {
 766            format!("{} {}", item.label, detail)
 767        } else {
 768            item.label.clone()
 769        };
 770        Some(language::CodeLabel::filtered(
 771            text,
 772            label_len,
 773            item.filter_text.as_deref(),
 774            vec![(0..label_len, highlight_id)],
 775        ))
 776    }
 777
 778    async fn initialization_options(
 779        self: Arc<Self>,
 780        adapter: &Arc<dyn LspAdapterDelegate>,
 781    ) -> Result<Option<serde_json::Value>> {
 782        let tsdk_path = self.tsdk_path(adapter).await;
 783        Ok(Some(json!({
 784            "provideFormatter": true,
 785            "hostInfo": "zed",
 786            "tsserver": {
 787                "path": tsdk_path,
 788            },
 789            "preferences": {
 790                "includeInlayParameterNameHints": "all",
 791                "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
 792                "includeInlayFunctionParameterTypeHints": true,
 793                "includeInlayVariableTypeHints": true,
 794                "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
 795                "includeInlayPropertyDeclarationTypeHints": true,
 796                "includeInlayFunctionLikeReturnTypeHints": true,
 797                "includeInlayEnumMemberValueHints": true,
 798            }
 799        })))
 800    }
 801
 802    async fn workspace_configuration(
 803        self: Arc<Self>,
 804        delegate: &Arc<dyn LspAdapterDelegate>,
 805        _: Option<Toolchain>,
 806        _: Option<Uri>,
 807        cx: &mut AsyncApp,
 808    ) -> Result<Value> {
 809        let override_options = cx.update(|cx| {
 810            language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
 811                .and_then(|s| s.settings.clone())
 812        })?;
 813        if let Some(options) = override_options {
 814            return Ok(options);
 815        }
 816        Ok(json!({
 817            "completions": {
 818              "completeFunctionCalls": true
 819            }
 820        }))
 821    }
 822
 823    fn language_ids(&self) -> HashMap<LanguageName, String> {
 824        HashMap::from_iter([
 825            (LanguageName::new("TypeScript"), "typescript".into()),
 826            (LanguageName::new("JavaScript"), "javascript".into()),
 827            (LanguageName::new("TSX"), "typescriptreact".into()),
 828        ])
 829    }
 830}
 831
 832async fn get_cached_ts_server_binary(
 833    container_dir: PathBuf,
 834    node: &NodeRuntime,
 835) -> Option<LanguageServerBinary> {
 836    maybe!(async {
 837        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
 838        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
 839        if new_server_path.exists() {
 840            Ok(LanguageServerBinary {
 841                path: node.binary_path().await?,
 842                env: None,
 843                arguments: typescript_server_binary_arguments(&new_server_path),
 844            })
 845        } else if old_server_path.exists() {
 846            Ok(LanguageServerBinary {
 847                path: node.binary_path().await?,
 848                env: None,
 849                arguments: typescript_server_binary_arguments(&old_server_path),
 850            })
 851        } else {
 852            anyhow::bail!("missing executable in directory {container_dir:?}")
 853        }
 854    })
 855    .await
 856    .log_err()
 857}
 858
 859#[cfg(test)]
 860mod tests {
 861    use std::path::Path;
 862
 863    use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
 864    use project::FakeFs;
 865    use serde_json::json;
 866    use task::TaskTemplates;
 867    use unindent::Unindent;
 868    use util::{path, rel_path::rel_path};
 869
 870    use crate::typescript::{
 871        PackageJsonData, TypeScriptContextProvider, replace_test_name_parameters,
 872    };
 873
 874    #[gpui::test]
 875    async fn test_outline(cx: &mut TestAppContext) {
 876        for language in [
 877            crate::language(
 878                "typescript",
 879                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
 880            ),
 881            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
 882        ] {
 883            let text = r#"
 884            function a() {
 885              // local variables are included
 886              let a1 = 1;
 887              // all functions are included
 888              async function a2() {}
 889            }
 890            // top-level variables are included
 891            let b: C
 892            function getB() {}
 893            // exported variables are included
 894            export const d = e;
 895        "#
 896            .unindent();
 897
 898            let buffer = cx
 899                .new(|cx| language::Buffer::local(text, cx).with_language_immediate(language, cx));
 900            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
 901            assert_eq!(
 902                outline
 903                    .items
 904                    .iter()
 905                    .map(|item| (item.text.as_str(), item.depth))
 906                    .collect::<Vec<_>>(),
 907                &[
 908                    ("function a()", 0),
 909                    ("let a1", 1),
 910                    ("async function a2()", 1),
 911                    ("let b", 0),
 912                    ("function getB()", 0),
 913                    ("const d", 0),
 914                ]
 915            );
 916        }
 917    }
 918
 919    #[gpui::test]
 920    async fn test_outline_with_destructuring(cx: &mut TestAppContext) {
 921        for language in [
 922            crate::language(
 923                "typescript",
 924                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
 925            ),
 926            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
 927        ] {
 928            let text = r#"
 929            // Top-level destructuring
 930            const { a1, a2 } = a;
 931            const [b1, b2] = b;
 932
 933            // Defaults and rest
 934            const [c1 = 1, , c2, ...rest1] = c;
 935            const { d1, d2: e1, f1 = 2, g1: h1 = 3, ...rest2 } = d;
 936
 937            function processData() {
 938              // Nested object destructuring
 939              const { c1, c2 } = c;
 940              // Nested array destructuring
 941              const [d1, d2, d3] = d;
 942              // Destructuring with renaming
 943              const { f1: g1 } = f;
 944              // With defaults
 945              const [x = 10, y] = xy;
 946            }
 947
 948            class DataHandler {
 949              method() {
 950                // Destructuring in class method
 951                const { a1, a2 } = a;
 952                const [b1, ...b2] = b;
 953              }
 954            }
 955        "#
 956            .unindent();
 957
 958            let buffer = cx
 959                .new(|cx| language::Buffer::local(text, cx).with_language_immediate(language, cx));
 960            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
 961            assert_eq!(
 962                outline
 963                    .items
 964                    .iter()
 965                    .map(|item| (item.text.as_str(), item.depth))
 966                    .collect::<Vec<_>>(),
 967                &[
 968                    ("const a1", 0),
 969                    ("const a2", 0),
 970                    ("const b1", 0),
 971                    ("const b2", 0),
 972                    ("const c1", 0),
 973                    ("const c2", 0),
 974                    ("const rest1", 0),
 975                    ("const d1", 0),
 976                    ("const e1", 0),
 977                    ("const h1", 0),
 978                    ("const rest2", 0),
 979                    ("function processData()", 0),
 980                    ("const c1", 1),
 981                    ("const c2", 1),
 982                    ("const d1", 1),
 983                    ("const d2", 1),
 984                    ("const d3", 1),
 985                    ("const g1", 1),
 986                    ("const x", 1),
 987                    ("const y", 1),
 988                    ("class DataHandler", 0),
 989                    ("method()", 1),
 990                    ("const a1", 2),
 991                    ("const a2", 2),
 992                    ("const b1", 2),
 993                    ("const b2", 2),
 994                ]
 995            );
 996        }
 997    }
 998
 999    #[gpui::test]
1000    async fn test_outline_with_object_properties(cx: &mut TestAppContext) {
1001        for language in [
1002            crate::language(
1003                "typescript",
1004                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1005            ),
1006            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1007        ] {
1008            let text = r#"
1009            // Object with function properties
1010            const o = { m() {}, async n() {}, g: function* () {}, h: () => {}, k: function () {} };
1011
1012            // Object with primitive properties
1013            const p = { p1: 1, p2: "hello", p3: true };
1014
1015            // Nested objects
1016            const q = {
1017                r: {
1018                    // won't be included due to one-level depth limit
1019                    s: 1
1020                },
1021                t: 2
1022            };
1023
1024            function getData() {
1025                const local = { x: 1, y: 2 };
1026                return local;
1027            }
1028        "#
1029            .unindent();
1030
1031            let buffer = cx
1032                .new(|cx| language::Buffer::local(text, cx).with_language_immediate(language, cx));
1033            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1034            assert_eq!(
1035                outline
1036                    .items
1037                    .iter()
1038                    .map(|item| (item.text.as_str(), item.depth))
1039                    .collect::<Vec<_>>(),
1040                &[
1041                    ("const o", 0),
1042                    ("m()", 1),
1043                    ("async n()", 1),
1044                    ("g", 1),
1045                    ("h", 1),
1046                    ("k", 1),
1047                    ("const p", 0),
1048                    ("p1", 1),
1049                    ("p2", 1),
1050                    ("p3", 1),
1051                    ("const q", 0),
1052                    ("r", 1),
1053                    ("s", 2),
1054                    ("t", 1),
1055                    ("function getData()", 0),
1056                    ("const local", 1),
1057                    ("x", 2),
1058                    ("y", 2),
1059                ]
1060            );
1061        }
1062    }
1063
1064    #[gpui::test]
1065    async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) {
1066        for language in [
1067            crate::language(
1068                "typescript",
1069                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1070            ),
1071            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1072        ] {
1073            let text = r#"
1074            // Symbols as object keys
1075            const sym = Symbol("test");
1076            const obj1 = {
1077                [sym]: 1,
1078                [Symbol("inline")]: 2,
1079                normalKey: 3
1080            };
1081
1082            // Enums as object keys
1083            enum Color { Red, Blue, Green }
1084
1085            const obj2 = {
1086                [Color.Red]: "red value",
1087                [Color.Blue]: "blue value",
1088                regularProp: "normal"
1089            };
1090
1091            // Mixed computed properties
1092            const key = "dynamic";
1093            const obj3 = {
1094                [key]: 1,
1095                ["string" + "concat"]: 2,
1096                [1 + 1]: 3,
1097                static: 4
1098            };
1099
1100            // Nested objects with computed properties
1101            const obj4 = {
1102                [sym]: {
1103                    nested: 1
1104                },
1105                regular: {
1106                    [key]: 2
1107                }
1108            };
1109        "#
1110            .unindent();
1111
1112            let buffer = cx
1113                .new(|cx| language::Buffer::local(text, cx).with_language_immediate(language, cx));
1114            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1115            assert_eq!(
1116                outline
1117                    .items
1118                    .iter()
1119                    .map(|item| (item.text.as_str(), item.depth))
1120                    .collect::<Vec<_>>(),
1121                &[
1122                    ("const sym", 0),
1123                    ("const obj1", 0),
1124                    ("[sym]", 1),
1125                    ("[Symbol(\"inline\")]", 1),
1126                    ("normalKey", 1),
1127                    ("enum Color", 0),
1128                    ("const obj2", 0),
1129                    ("[Color.Red]", 1),
1130                    ("[Color.Blue]", 1),
1131                    ("regularProp", 1),
1132                    ("const key", 0),
1133                    ("const obj3", 0),
1134                    ("[key]", 1),
1135                    ("[\"string\" + \"concat\"]", 1),
1136                    ("[1 + 1]", 1),
1137                    ("static", 1),
1138                    ("const obj4", 0),
1139                    ("[sym]", 1),
1140                    ("nested", 2),
1141                    ("regular", 1),
1142                    ("[key]", 2),
1143                ]
1144            );
1145        }
1146    }
1147
1148    #[gpui::test]
1149    async fn test_generator_function_outline(cx: &mut TestAppContext) {
1150        let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
1151
1152        let text = r#"
1153            function normalFunction() {
1154                console.log("normal");
1155            }
1156
1157            function* simpleGenerator() {
1158                yield 1;
1159                yield 2;
1160            }
1161
1162            async function* asyncGenerator() {
1163                yield await Promise.resolve(1);
1164            }
1165
1166            function* generatorWithParams(start, end) {
1167                for (let i = start; i <= end; i++) {
1168                    yield i;
1169                }
1170            }
1171
1172            class TestClass {
1173                *methodGenerator() {
1174                    yield "method";
1175                }
1176
1177                async *asyncMethodGenerator() {
1178                    yield "async method";
1179                }
1180            }
1181        "#
1182        .unindent();
1183
1184        let buffer =
1185            cx.new(|cx| language::Buffer::local(text, cx).with_language_immediate(language, cx));
1186        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1187        assert_eq!(
1188            outline
1189                .items
1190                .iter()
1191                .map(|item| (item.text.as_str(), item.depth))
1192                .collect::<Vec<_>>(),
1193            &[
1194                ("function normalFunction()", 0),
1195                ("function* simpleGenerator()", 0),
1196                ("async function* asyncGenerator()", 0),
1197                ("function* generatorWithParams( )", 0),
1198                ("class TestClass", 0),
1199                ("*methodGenerator()", 1),
1200                ("async *asyncMethodGenerator()", 1),
1201            ]
1202        );
1203    }
1204
1205    #[gpui::test]
1206    async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1207        cx.update(|cx| {
1208            settings::init(cx);
1209        });
1210
1211        let package_json_1 = json!({
1212            "dependencies": {
1213                "mocha": "1.0.0",
1214                "vitest": "1.0.0"
1215            },
1216            "scripts": {
1217                "test": ""
1218            }
1219        })
1220        .to_string();
1221
1222        let package_json_2 = json!({
1223            "devDependencies": {
1224                "vitest": "2.0.0"
1225            },
1226            "scripts": {
1227                "test": ""
1228            }
1229        })
1230        .to_string();
1231
1232        let fs = FakeFs::new(executor);
1233        fs.insert_tree(
1234            path!("/root"),
1235            json!({
1236                "package.json": package_json_1,
1237                "sub": {
1238                    "package.json": package_json_2,
1239                    "file.js": "",
1240                }
1241            }),
1242        )
1243        .await;
1244
1245        let provider = TypeScriptContextProvider::new(fs.clone());
1246        let package_json_data = cx
1247            .update(|cx| {
1248                provider.combined_package_json_data(
1249                    fs.clone(),
1250                    path!("/root").as_ref(),
1251                    rel_path("sub/file1.js"),
1252                    cx,
1253                )
1254            })
1255            .await
1256            .unwrap();
1257        pretty_assertions::assert_eq!(
1258            package_json_data,
1259            PackageJsonData {
1260                jest_package_path: None,
1261                mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
1262                vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
1263                jasmine_package_path: None,
1264                bun_package_path: None,
1265                node_package_path: None,
1266                scripts: [
1267                    (
1268                        Path::new(path!("/root/package.json")).into(),
1269                        "test".to_owned()
1270                    ),
1271                    (
1272                        Path::new(path!("/root/sub/package.json")).into(),
1273                        "test".to_owned()
1274                    )
1275                ]
1276                .into_iter()
1277                .collect(),
1278                package_manager: None,
1279            }
1280        );
1281
1282        let mut task_templates = TaskTemplates::default();
1283        package_json_data.fill_task_templates(&mut task_templates);
1284        let task_templates = task_templates
1285            .0
1286            .into_iter()
1287            .map(|template| (template.label, template.cwd))
1288            .collect::<Vec<_>>();
1289        pretty_assertions::assert_eq!(
1290            task_templates,
1291            [
1292                (
1293                    "vitest file test".into(),
1294                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1295                ),
1296                (
1297                    "vitest test $ZED_SYMBOL".into(),
1298                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1299                ),
1300                (
1301                    "mocha file test".into(),
1302                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1303                ),
1304                (
1305                    "mocha test $ZED_SYMBOL".into(),
1306                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1307                ),
1308                (
1309                    "root/package.json > test".into(),
1310                    Some(path!("/root").into())
1311                ),
1312                (
1313                    "sub/package.json > test".into(),
1314                    Some(path!("/root/sub").into())
1315                ),
1316            ]
1317        );
1318    }
1319
1320    #[test]
1321    fn test_escaping_name() {
1322        let cases = [
1323            ("plain test name", "plain test name"),
1324            ("test name with $param_name", "test name with (.+?)"),
1325            ("test name with $nested.param.name", "test name with (.+?)"),
1326            ("test name with $#", "test name with (.+?)"),
1327            ("test name with $##", "test name with (.+?)\\#"),
1328            ("test name with %p", "test name with (.+?)"),
1329            ("test name with %s", "test name with (.+?)"),
1330            ("test name with %d", "test name with (.+?)"),
1331            ("test name with %i", "test name with (.+?)"),
1332            ("test name with %f", "test name with (.+?)"),
1333            ("test name with %j", "test name with (.+?)"),
1334            ("test name with %o", "test name with (.+?)"),
1335            ("test name with %#", "test name with (.+?)"),
1336            ("test name with %$", "test name with (.+?)"),
1337            ("test name with %%", "test name with (.+?)"),
1338            ("test name with %q", "test name with %q"),
1339            (
1340                "test name with regex chars .*+?^${}()|[]\\",
1341                "test name with regex chars \\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\",
1342            ),
1343            (
1344                "test name with multiple $params and %pretty and %b and (.+?)",
1345                "test name with multiple (.+?) and (.+?)retty and %b and \\(\\.\\+\\?\\)",
1346            ),
1347        ];
1348
1349        for (input, expected) in cases {
1350            assert_eq!(replace_test_name_parameters(input), expected);
1351        }
1352    }
1353
1354    // The order of test runner tasks is based on inferred user preference:
1355    // 1. Dedicated test runners (e.g., Jest, Vitest, Mocha, Jasmine) are prioritized.
1356    // 2. Bun's built-in test runner (`bun test`) comes next.
1357    // 3. Node.js's built-in test runner (`node --test`) is last.
1358    // This hierarchy assumes that if a dedicated test framework is installed, it is the
1359    // preferred testing mechanism. Between runtime-specific options, `bun test` is
1360    // typically preferred over `node --test` when @types/bun is present.
1361    #[gpui::test]
1362    async fn test_task_ordering_with_multiple_test_runners(
1363        executor: BackgroundExecutor,
1364        cx: &mut TestAppContext,
1365    ) {
1366        cx.update(|cx| {
1367            settings::init(cx);
1368        });
1369
1370        // Test case with all test runners present
1371        let package_json_all_runners = json!({
1372            "devDependencies": {
1373                "@types/bun": "1.0.0",
1374                "@types/node": "^20.0.0",
1375                "jest": "29.0.0",
1376                "mocha": "10.0.0",
1377                "vitest": "1.0.0",
1378                "jasmine": "5.0.0",
1379            },
1380            "scripts": {
1381                "test": "jest"
1382            }
1383        })
1384        .to_string();
1385
1386        let fs = FakeFs::new(executor);
1387        fs.insert_tree(
1388            path!("/root"),
1389            json!({
1390                "package.json": package_json_all_runners,
1391                "file.js": "",
1392            }),
1393        )
1394        .await;
1395
1396        let provider = TypeScriptContextProvider::new(fs.clone());
1397
1398        let package_json_data = cx
1399            .update(|cx| {
1400                provider.combined_package_json_data(
1401                    fs.clone(),
1402                    path!("/root").as_ref(),
1403                    rel_path("file.js"),
1404                    cx,
1405                )
1406            })
1407            .await
1408            .unwrap();
1409
1410        assert!(package_json_data.jest_package_path.is_some());
1411        assert!(package_json_data.mocha_package_path.is_some());
1412        assert!(package_json_data.vitest_package_path.is_some());
1413        assert!(package_json_data.jasmine_package_path.is_some());
1414        assert!(package_json_data.bun_package_path.is_some());
1415        assert!(package_json_data.node_package_path.is_some());
1416
1417        let mut task_templates = TaskTemplates::default();
1418        package_json_data.fill_task_templates(&mut task_templates);
1419
1420        let test_tasks: Vec<_> = task_templates
1421            .0
1422            .iter()
1423            .filter(|template| {
1424                template.tags.contains(&"ts-test".to_owned())
1425                    || template.tags.contains(&"js-test".to_owned())
1426            })
1427            .map(|template| &template.label)
1428            .collect();
1429
1430        let node_test_index = test_tasks
1431            .iter()
1432            .position(|label| label.contains("node test"));
1433        let jest_test_index = test_tasks.iter().position(|label| label.contains("jest"));
1434        let bun_test_index = test_tasks
1435            .iter()
1436            .position(|label| label.contains("bun test"));
1437
1438        assert!(
1439            node_test_index.is_some(),
1440            "Node test tasks should be present"
1441        );
1442        assert!(
1443            jest_test_index.is_some(),
1444            "Jest test tasks should be present"
1445        );
1446        assert!(bun_test_index.is_some(), "Bun test tasks should be present");
1447
1448        assert!(
1449            jest_test_index.unwrap() < bun_test_index.unwrap(),
1450            "Jest should come before Bun"
1451        );
1452        assert!(
1453            bun_test_index.unwrap() < node_test_index.unwrap(),
1454            "Bun should come before Node"
1455        );
1456    }
1457}