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: &str = "node_modules/typescript-language-server/lib/cli.js";
 603    const NEW_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.mjs";
 604
 605    const PACKAGE_NAME: &str = "typescript";
 606    const SERVER_PACKAGE_NAME: &str = "typescript-language-server";
 607
 608    const SERVER_NAME: LanguageServerName =
 609        LanguageServerName::new_static(Self::SERVER_PACKAGE_NAME);
 610
 611    pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
 612        TypeScriptLspAdapter { fs, node }
 613    }
 614
 615    async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
 616        let is_yarn = adapter
 617            .read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap())
 618            .await
 619            .is_ok();
 620
 621        let tsdk_path = if is_yarn {
 622            ".yarn/sdks/typescript/lib"
 623        } else {
 624            "node_modules/typescript/lib"
 625        };
 626
 627        if self
 628            .fs
 629            .is_dir(&adapter.worktree_root_path().join(tsdk_path))
 630            .await
 631        {
 632            Some(tsdk_path)
 633        } else {
 634            None
 635        }
 636    }
 637}
 638
 639pub struct TypeScriptVersions {
 640    typescript_version: String,
 641    server_version: String,
 642}
 643
 644impl LspInstaller for TypeScriptLspAdapter {
 645    type BinaryVersion = TypeScriptVersions;
 646
 647    async fn fetch_latest_server_version(
 648        &self,
 649        _: &dyn LspAdapterDelegate,
 650        _: bool,
 651        _: &mut AsyncApp,
 652    ) -> Result<TypeScriptVersions> {
 653        Ok(TypeScriptVersions {
 654            typescript_version: self
 655                .node
 656                .npm_package_latest_version(Self::PACKAGE_NAME)
 657                .await?,
 658            server_version: self
 659                .node
 660                .npm_package_latest_version(Self::SERVER_PACKAGE_NAME)
 661                .await?,
 662        })
 663    }
 664
 665    async fn check_if_version_installed(
 666        &self,
 667        version: &TypeScriptVersions,
 668        container_dir: &PathBuf,
 669        _: &dyn LspAdapterDelegate,
 670    ) -> Option<LanguageServerBinary> {
 671        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 672
 673        if self
 674            .node
 675            .should_install_npm_package(
 676                Self::PACKAGE_NAME,
 677                &server_path,
 678                container_dir,
 679                VersionStrategy::Latest(version.typescript_version.as_str()),
 680            )
 681            .await
 682        {
 683            return None;
 684        }
 685
 686        if self
 687            .node
 688            .should_install_npm_package(
 689                Self::SERVER_PACKAGE_NAME,
 690                &server_path,
 691                container_dir,
 692                VersionStrategy::Latest(version.server_version.as_str()),
 693            )
 694            .await
 695        {
 696            return None;
 697        }
 698
 699        Some(LanguageServerBinary {
 700            path: self.node.binary_path().await.ok()?,
 701            env: None,
 702            arguments: typescript_server_binary_arguments(&server_path),
 703        })
 704    }
 705
 706    async fn fetch_server_binary(
 707        &self,
 708        latest_version: TypeScriptVersions,
 709        container_dir: PathBuf,
 710        _: &dyn LspAdapterDelegate,
 711    ) -> Result<LanguageServerBinary> {
 712        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 713
 714        self.node
 715            .npm_install_packages(
 716                &container_dir,
 717                &[
 718                    (
 719                        Self::PACKAGE_NAME,
 720                        latest_version.typescript_version.as_str(),
 721                    ),
 722                    (
 723                        Self::SERVER_PACKAGE_NAME,
 724                        latest_version.server_version.as_str(),
 725                    ),
 726                ],
 727            )
 728            .await?;
 729
 730        Ok(LanguageServerBinary {
 731            path: self.node.binary_path().await?,
 732            env: None,
 733            arguments: typescript_server_binary_arguments(&server_path),
 734        })
 735    }
 736
 737    async fn cached_server_binary(
 738        &self,
 739        container_dir: PathBuf,
 740        _: &dyn LspAdapterDelegate,
 741    ) -> Option<LanguageServerBinary> {
 742        get_cached_ts_server_binary(container_dir, &self.node).await
 743    }
 744}
 745
 746#[async_trait(?Send)]
 747impl LspAdapter for TypeScriptLspAdapter {
 748    fn name(&self) -> LanguageServerName {
 749        Self::SERVER_NAME
 750    }
 751
 752    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
 753        Some(vec![
 754            CodeActionKind::QUICKFIX,
 755            CodeActionKind::REFACTOR,
 756            CodeActionKind::REFACTOR_EXTRACT,
 757            CodeActionKind::SOURCE,
 758        ])
 759    }
 760
 761    async fn label_for_completion(
 762        &self,
 763        item: &lsp::CompletionItem,
 764        language: &Arc<language::Language>,
 765    ) -> Option<language::CodeLabel> {
 766        use lsp::CompletionItemKind as Kind;
 767        let label_len = item.label.len();
 768        let grammar = language.grammar()?;
 769        let highlight_id = match item.kind? {
 770            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
 771            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
 772            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
 773            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
 774            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
 775            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
 776            _ => None,
 777        }?;
 778
 779        let text = if let Some(description) = item
 780            .label_details
 781            .as_ref()
 782            .and_then(|label_details| label_details.description.as_ref())
 783        {
 784            format!("{} {}", item.label, description)
 785        } else if let Some(detail) = &item.detail {
 786            format!("{} {}", item.label, detail)
 787        } else {
 788            item.label.clone()
 789        };
 790        Some(language::CodeLabel::filtered(
 791            text,
 792            label_len,
 793            item.filter_text.as_deref(),
 794            vec![(0..label_len, highlight_id)],
 795        ))
 796    }
 797
 798    async fn initialization_options(
 799        self: Arc<Self>,
 800        adapter: &Arc<dyn LspAdapterDelegate>,
 801    ) -> Result<Option<serde_json::Value>> {
 802        let tsdk_path = self.tsdk_path(adapter).await;
 803        Ok(Some(json!({
 804            "provideFormatter": true,
 805            "hostInfo": "zed",
 806            "tsserver": {
 807                "path": tsdk_path,
 808            },
 809            "preferences": {
 810                "includeInlayParameterNameHints": "all",
 811                "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
 812                "includeInlayFunctionParameterTypeHints": true,
 813                "includeInlayVariableTypeHints": true,
 814                "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
 815                "includeInlayPropertyDeclarationTypeHints": true,
 816                "includeInlayFunctionLikeReturnTypeHints": true,
 817                "includeInlayEnumMemberValueHints": true,
 818            }
 819        })))
 820    }
 821
 822    async fn workspace_configuration(
 823        self: Arc<Self>,
 824        delegate: &Arc<dyn LspAdapterDelegate>,
 825        _: Option<Toolchain>,
 826        _: Option<Uri>,
 827        cx: &mut AsyncApp,
 828    ) -> Result<Value> {
 829        let override_options = cx.update(|cx| {
 830            language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
 831                .and_then(|s| s.settings.clone())
 832        })?;
 833        if let Some(options) = override_options {
 834            return Ok(options);
 835        }
 836        Ok(json!({
 837            "completions": {
 838              "completeFunctionCalls": true
 839            }
 840        }))
 841    }
 842
 843    fn language_ids(&self) -> HashMap<LanguageName, String> {
 844        HashMap::from_iter([
 845            (LanguageName::new_static("TypeScript"), "typescript".into()),
 846            (LanguageName::new_static("JavaScript"), "javascript".into()),
 847            (LanguageName::new_static("TSX"), "typescriptreact".into()),
 848        ])
 849    }
 850}
 851
 852async fn get_cached_ts_server_binary(
 853    container_dir: PathBuf,
 854    node: &NodeRuntime,
 855) -> Option<LanguageServerBinary> {
 856    maybe!(async {
 857        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
 858        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
 859        if new_server_path.exists() {
 860            Ok(LanguageServerBinary {
 861                path: node.binary_path().await?,
 862                env: None,
 863                arguments: typescript_server_binary_arguments(&new_server_path),
 864            })
 865        } else if old_server_path.exists() {
 866            Ok(LanguageServerBinary {
 867                path: node.binary_path().await?,
 868                env: None,
 869                arguments: typescript_server_binary_arguments(&old_server_path),
 870            })
 871        } else {
 872            anyhow::bail!("missing executable in directory {container_dir:?}")
 873        }
 874    })
 875    .await
 876    .log_err()
 877}
 878
 879#[cfg(test)]
 880mod tests {
 881    use std::path::Path;
 882
 883    use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
 884    use project::FakeFs;
 885    use serde_json::json;
 886    use task::TaskTemplates;
 887    use unindent::Unindent;
 888    use util::{path, rel_path::rel_path};
 889
 890    use crate::typescript::{
 891        PackageJsonData, TypeScriptContextProvider, replace_test_name_parameters,
 892    };
 893
 894    #[gpui::test]
 895    async fn test_outline(cx: &mut TestAppContext) {
 896        for language in [
 897            crate::language(
 898                "typescript",
 899                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
 900            ),
 901            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
 902        ] {
 903            let text = r#"
 904            function a() {
 905              // local variables are included
 906              let a1 = 1;
 907              // all functions are included
 908              async function a2() {}
 909            }
 910            // top-level variables are included
 911            let b: C
 912            function getB() {}
 913            // exported variables are included
 914            export const d = e;
 915        "#
 916            .unindent();
 917
 918            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
 919            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
 920            assert_eq!(
 921                outline
 922                    .items
 923                    .iter()
 924                    .map(|item| (item.text.as_str(), item.depth))
 925                    .collect::<Vec<_>>(),
 926                &[
 927                    ("function a()", 0),
 928                    ("let a1", 1),
 929                    ("async function a2()", 1),
 930                    ("let b", 0),
 931                    ("function getB()", 0),
 932                    ("const d", 0),
 933                ]
 934            );
 935        }
 936    }
 937
 938    #[gpui::test]
 939    async fn test_outline_with_destructuring(cx: &mut TestAppContext) {
 940        for language in [
 941            crate::language(
 942                "typescript",
 943                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
 944            ),
 945            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
 946        ] {
 947            let text = r#"
 948            // Top-level destructuring
 949            const { a1, a2 } = a;
 950            const [b1, b2] = b;
 951
 952            // Defaults and rest
 953            const [c1 = 1, , c2, ...rest1] = c;
 954            const { d1, d2: e1, f1 = 2, g1: h1 = 3, ...rest2 } = d;
 955
 956            function processData() {
 957              // Nested object destructuring
 958              const { c1, c2 } = c;
 959              // Nested array destructuring
 960              const [d1, d2, d3] = d;
 961              // Destructuring with renaming
 962              const { f1: g1 } = f;
 963              // With defaults
 964              const [x = 10, y] = xy;
 965            }
 966
 967            class DataHandler {
 968              method() {
 969                // Destructuring in class method
 970                const { a1, a2 } = a;
 971                const [b1, ...b2] = b;
 972              }
 973            }
 974        "#
 975            .unindent();
 976
 977            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
 978            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
 979            assert_eq!(
 980                outline
 981                    .items
 982                    .iter()
 983                    .map(|item| (item.text.as_str(), item.depth))
 984                    .collect::<Vec<_>>(),
 985                &[
 986                    ("const a1", 0),
 987                    ("const a2", 0),
 988                    ("const b1", 0),
 989                    ("const b2", 0),
 990                    ("const c1", 0),
 991                    ("const c2", 0),
 992                    ("const rest1", 0),
 993                    ("const d1", 0),
 994                    ("const e1", 0),
 995                    ("const h1", 0),
 996                    ("const rest2", 0),
 997                    ("function processData()", 0),
 998                    ("const c1", 1),
 999                    ("const c2", 1),
1000                    ("const d1", 1),
1001                    ("const d2", 1),
1002                    ("const d3", 1),
1003                    ("const g1", 1),
1004                    ("const x", 1),
1005                    ("const y", 1),
1006                    ("class DataHandler", 0),
1007                    ("method()", 1),
1008                    ("const a1", 2),
1009                    ("const a2", 2),
1010                    ("const b1", 2),
1011                    ("const b2", 2),
1012                ]
1013            );
1014        }
1015    }
1016
1017    #[gpui::test]
1018    async fn test_outline_with_object_properties(cx: &mut TestAppContext) {
1019        for language in [
1020            crate::language(
1021                "typescript",
1022                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1023            ),
1024            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1025        ] {
1026            let text = r#"
1027            // Object with function properties
1028            const o = { m() {}, async n() {}, g: function* () {}, h: () => {}, k: function () {} };
1029
1030            // Object with primitive properties
1031            const p = { p1: 1, p2: "hello", p3: true };
1032
1033            // Nested objects
1034            const q = {
1035                r: {
1036                    // won't be included due to one-level depth limit
1037                    s: 1
1038                },
1039                t: 2
1040            };
1041
1042            function getData() {
1043                const local = { x: 1, y: 2 };
1044                return local;
1045            }
1046        "#
1047            .unindent();
1048
1049            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1050            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1051            assert_eq!(
1052                outline
1053                    .items
1054                    .iter()
1055                    .map(|item| (item.text.as_str(), item.depth))
1056                    .collect::<Vec<_>>(),
1057                &[
1058                    ("const o", 0),
1059                    ("m()", 1),
1060                    ("async n()", 1),
1061                    ("g", 1),
1062                    ("h", 1),
1063                    ("k", 1),
1064                    ("const p", 0),
1065                    ("p1", 1),
1066                    ("p2", 1),
1067                    ("p3", 1),
1068                    ("const q", 0),
1069                    ("r", 1),
1070                    ("s", 2),
1071                    ("t", 1),
1072                    ("function getData()", 0),
1073                    ("const local", 1),
1074                    ("x", 2),
1075                    ("y", 2),
1076                ]
1077            );
1078        }
1079    }
1080
1081    #[gpui::test]
1082    async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) {
1083        for language in [
1084            crate::language(
1085                "typescript",
1086                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1087            ),
1088            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1089        ] {
1090            let text = r#"
1091            // Symbols as object keys
1092            const sym = Symbol("test");
1093            const obj1 = {
1094                [sym]: 1,
1095                [Symbol("inline")]: 2,
1096                normalKey: 3
1097            };
1098
1099            // Enums as object keys
1100            enum Color { Red, Blue, Green }
1101
1102            const obj2 = {
1103                [Color.Red]: "red value",
1104                [Color.Blue]: "blue value",
1105                regularProp: "normal"
1106            };
1107
1108            // Mixed computed properties
1109            const key = "dynamic";
1110            const obj3 = {
1111                [key]: 1,
1112                ["string" + "concat"]: 2,
1113                [1 + 1]: 3,
1114                static: 4
1115            };
1116
1117            // Nested objects with computed properties
1118            const obj4 = {
1119                [sym]: {
1120                    nested: 1
1121                },
1122                regular: {
1123                    [key]: 2
1124                }
1125            };
1126        "#
1127            .unindent();
1128
1129            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1130            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1131            assert_eq!(
1132                outline
1133                    .items
1134                    .iter()
1135                    .map(|item| (item.text.as_str(), item.depth))
1136                    .collect::<Vec<_>>(),
1137                &[
1138                    ("const sym", 0),
1139                    ("const obj1", 0),
1140                    ("[sym]", 1),
1141                    ("[Symbol(\"inline\")]", 1),
1142                    ("normalKey", 1),
1143                    ("enum Color", 0),
1144                    ("const obj2", 0),
1145                    ("[Color.Red]", 1),
1146                    ("[Color.Blue]", 1),
1147                    ("regularProp", 1),
1148                    ("const key", 0),
1149                    ("const obj3", 0),
1150                    ("[key]", 1),
1151                    ("[\"string\" + \"concat\"]", 1),
1152                    ("[1 + 1]", 1),
1153                    ("static", 1),
1154                    ("const obj4", 0),
1155                    ("[sym]", 1),
1156                    ("nested", 2),
1157                    ("regular", 1),
1158                    ("[key]", 2),
1159                ]
1160            );
1161        }
1162    }
1163
1164    #[gpui::test]
1165    async fn test_generator_function_outline(cx: &mut TestAppContext) {
1166        let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
1167
1168        let text = r#"
1169            function normalFunction() {
1170                console.log("normal");
1171            }
1172
1173            function* simpleGenerator() {
1174                yield 1;
1175                yield 2;
1176            }
1177
1178            async function* asyncGenerator() {
1179                yield await Promise.resolve(1);
1180            }
1181
1182            function* generatorWithParams(start, end) {
1183                for (let i = start; i <= end; i++) {
1184                    yield i;
1185                }
1186            }
1187
1188            class TestClass {
1189                *methodGenerator() {
1190                    yield "method";
1191                }
1192
1193                async *asyncMethodGenerator() {
1194                    yield "async method";
1195                }
1196            }
1197        "#
1198        .unindent();
1199
1200        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1201        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1202        assert_eq!(
1203            outline
1204                .items
1205                .iter()
1206                .map(|item| (item.text.as_str(), item.depth))
1207                .collect::<Vec<_>>(),
1208            &[
1209                ("function normalFunction()", 0),
1210                ("function* simpleGenerator()", 0),
1211                ("async function* asyncGenerator()", 0),
1212                ("function* generatorWithParams( )", 0),
1213                ("class TestClass", 0),
1214                ("*methodGenerator()", 1),
1215                ("async *asyncMethodGenerator()", 1),
1216            ]
1217        );
1218    }
1219
1220    #[gpui::test]
1221    async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1222        cx.update(|cx| {
1223            settings::init(cx);
1224        });
1225
1226        let package_json_1 = json!({
1227            "dependencies": {
1228                "mocha": "1.0.0",
1229                "vitest": "1.0.0"
1230            },
1231            "scripts": {
1232                "test": ""
1233            }
1234        })
1235        .to_string();
1236
1237        let package_json_2 = json!({
1238            "devDependencies": {
1239                "vitest": "2.0.0"
1240            },
1241            "scripts": {
1242                "test": ""
1243            }
1244        })
1245        .to_string();
1246
1247        let fs = FakeFs::new(executor);
1248        fs.insert_tree(
1249            path!("/root"),
1250            json!({
1251                "package.json": package_json_1,
1252                "sub": {
1253                    "package.json": package_json_2,
1254                    "file.js": "",
1255                }
1256            }),
1257        )
1258        .await;
1259
1260        let provider = TypeScriptContextProvider::new(fs.clone());
1261        let package_json_data = cx
1262            .update(|cx| {
1263                provider.combined_package_json_data(
1264                    fs.clone(),
1265                    path!("/root").as_ref(),
1266                    rel_path("sub/file1.js"),
1267                    cx,
1268                )
1269            })
1270            .await
1271            .unwrap();
1272        pretty_assertions::assert_eq!(
1273            package_json_data,
1274            PackageJsonData {
1275                jest_package_path: None,
1276                mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
1277                vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
1278                jasmine_package_path: None,
1279                bun_package_path: None,
1280                node_package_path: None,
1281                scripts: [
1282                    (
1283                        Path::new(path!("/root/package.json")).into(),
1284                        "test".to_owned()
1285                    ),
1286                    (
1287                        Path::new(path!("/root/sub/package.json")).into(),
1288                        "test".to_owned()
1289                    )
1290                ]
1291                .into_iter()
1292                .collect(),
1293                package_manager: None,
1294            }
1295        );
1296
1297        let mut task_templates = TaskTemplates::default();
1298        package_json_data.fill_task_templates(&mut task_templates);
1299        let task_templates = task_templates
1300            .0
1301            .into_iter()
1302            .map(|template| (template.label, template.cwd))
1303            .collect::<Vec<_>>();
1304        pretty_assertions::assert_eq!(
1305            task_templates,
1306            [
1307                (
1308                    "vitest file test".into(),
1309                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1310                ),
1311                (
1312                    "vitest test $ZED_SYMBOL".into(),
1313                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1314                ),
1315                (
1316                    "mocha file test".into(),
1317                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1318                ),
1319                (
1320                    "mocha test $ZED_SYMBOL".into(),
1321                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1322                ),
1323                (
1324                    "root/package.json > test".into(),
1325                    Some(path!("/root").into())
1326                ),
1327                (
1328                    "sub/package.json > test".into(),
1329                    Some(path!("/root/sub").into())
1330                ),
1331            ]
1332        );
1333    }
1334
1335    #[test]
1336    fn test_escaping_name() {
1337        let cases = [
1338            ("plain test name", "plain test name"),
1339            ("test name with $param_name", "test name with (.+?)"),
1340            ("test name with $nested.param.name", "test name with (.+?)"),
1341            ("test name with $#", "test name with (.+?)"),
1342            ("test name with $##", "test name with (.+?)\\#"),
1343            ("test name with %p", "test name with (.+?)"),
1344            ("test name with %s", "test name with (.+?)"),
1345            ("test name with %d", "test name with (.+?)"),
1346            ("test name with %i", "test name with (.+?)"),
1347            ("test name with %f", "test name with (.+?)"),
1348            ("test name with %j", "test name with (.+?)"),
1349            ("test name with %o", "test name with (.+?)"),
1350            ("test name with %#", "test name with (.+?)"),
1351            ("test name with %$", "test name with (.+?)"),
1352            ("test name with %%", "test name with (.+?)"),
1353            ("test name with %q", "test name with %q"),
1354            (
1355                "test name with regex chars .*+?^${}()|[]\\",
1356                "test name with regex chars \\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\",
1357            ),
1358            (
1359                "test name with multiple $params and %pretty and %b and (.+?)",
1360                "test name with multiple (.+?) and (.+?)retty and %b and \\(\\.\\+\\?\\)",
1361            ),
1362        ];
1363
1364        for (input, expected) in cases {
1365            assert_eq!(replace_test_name_parameters(input), expected);
1366        }
1367    }
1368
1369    // The order of test runner tasks is based on inferred user preference:
1370    // 1. Dedicated test runners (e.g., Jest, Vitest, Mocha, Jasmine) are prioritized.
1371    // 2. Bun's built-in test runner (`bun test`) comes next.
1372    // 3. Node.js's built-in test runner (`node --test`) is last.
1373    // This hierarchy assumes that if a dedicated test framework is installed, it is the
1374    // preferred testing mechanism. Between runtime-specific options, `bun test` is
1375    // typically preferred over `node --test` when @types/bun is present.
1376    #[gpui::test]
1377    async fn test_task_ordering_with_multiple_test_runners(
1378        executor: BackgroundExecutor,
1379        cx: &mut TestAppContext,
1380    ) {
1381        cx.update(|cx| {
1382            settings::init(cx);
1383        });
1384
1385        // Test case with all test runners present
1386        let package_json_all_runners = json!({
1387            "devDependencies": {
1388                "@types/bun": "1.0.0",
1389                "@types/node": "^20.0.0",
1390                "jest": "29.0.0",
1391                "mocha": "10.0.0",
1392                "vitest": "1.0.0",
1393                "jasmine": "5.0.0",
1394            },
1395            "scripts": {
1396                "test": "jest"
1397            }
1398        })
1399        .to_string();
1400
1401        let fs = FakeFs::new(executor);
1402        fs.insert_tree(
1403            path!("/root"),
1404            json!({
1405                "package.json": package_json_all_runners,
1406                "file.js": "",
1407            }),
1408        )
1409        .await;
1410
1411        let provider = TypeScriptContextProvider::new(fs.clone());
1412
1413        let package_json_data = cx
1414            .update(|cx| {
1415                provider.combined_package_json_data(
1416                    fs.clone(),
1417                    path!("/root").as_ref(),
1418                    rel_path("file.js"),
1419                    cx,
1420                )
1421            })
1422            .await
1423            .unwrap();
1424
1425        assert!(package_json_data.jest_package_path.is_some());
1426        assert!(package_json_data.mocha_package_path.is_some());
1427        assert!(package_json_data.vitest_package_path.is_some());
1428        assert!(package_json_data.jasmine_package_path.is_some());
1429        assert!(package_json_data.bun_package_path.is_some());
1430        assert!(package_json_data.node_package_path.is_some());
1431
1432        let mut task_templates = TaskTemplates::default();
1433        package_json_data.fill_task_templates(&mut task_templates);
1434
1435        let test_tasks: Vec<_> = task_templates
1436            .0
1437            .iter()
1438            .filter(|template| {
1439                template.tags.contains(&"ts-test".to_owned())
1440                    || template.tags.contains(&"js-test".to_owned())
1441            })
1442            .map(|template| &template.label)
1443            .collect();
1444
1445        let node_test_index = test_tasks
1446            .iter()
1447            .position(|label| label.contains("node test"));
1448        let jest_test_index = test_tasks.iter().position(|label| label.contains("jest"));
1449        let bun_test_index = test_tasks
1450            .iter()
1451            .position(|label| label.contains("bun test"));
1452
1453        assert!(
1454            node_test_index.is_some(),
1455            "Node test tasks should be present"
1456        );
1457        assert!(
1458            jest_test_index.is_some(),
1459            "Jest test tasks should be present"
1460        );
1461        assert!(bun_test_index.is_some(), "Bun test tasks should be present");
1462
1463        assert!(
1464            jest_test_index.unwrap() < bun_test_index.unwrap(),
1465            "Jest should come before Bun"
1466        );
1467        assert!(
1468            bun_test_index.unwrap() < node_test_index.unwrap(),
1469            "Bun should come before Node"
1470        );
1471    }
1472}