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