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