typescript.rs

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