typescript.rs

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