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, Entity, Task};
   7use itertools::Itertools as _;
   8use language::{
   9    Buffer, ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore,
  10    LspAdapter, 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        buffer: Option<Entity<Buffer>>,
 429        cx: &App,
 430    ) -> Task<Option<TaskTemplates>> {
 431        let file = buffer.and_then(|buffer| buffer.read(cx).file());
 432        let Some(file) = project::File::from_dyn(file).cloned() else {
 433            return Task::ready(None);
 434        };
 435        let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
 436            return Task::ready(None);
 437        };
 438        let file_relative_path = file.path().clone();
 439        let package_json_data = self.combined_package_json_data(
 440            self.fs.clone(),
 441            &worktree_root,
 442            &file_relative_path,
 443            cx,
 444        );
 445
 446        cx.background_spawn(async move {
 447            let mut task_templates = TaskTemplates(Vec::new());
 448            task_templates.0.push(TaskTemplate {
 449                label: format!(
 450                    "execute selection {}",
 451                    VariableName::SelectedText.template_value()
 452                ),
 453                command: "node".to_owned(),
 454                args: vec![
 455                    "-e".to_owned(),
 456                    format!("\"{}\"", VariableName::SelectedText.template_value()),
 457                ],
 458                ..TaskTemplate::default()
 459            });
 460
 461            match package_json_data.await {
 462                Ok(package_json) => {
 463                    package_json.fill_task_templates(&mut task_templates);
 464                }
 465                Err(e) => {
 466                    log::error!(
 467                        "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
 468                    );
 469                }
 470            }
 471
 472            Some(task_templates)
 473        })
 474    }
 475
 476    fn build_context(
 477        &self,
 478        current_vars: &task::TaskVariables,
 479        location: ContextLocation<'_>,
 480        _project_env: Option<HashMap<String, String>>,
 481        _toolchains: Arc<dyn LanguageToolchainStore>,
 482        cx: &mut App,
 483    ) -> Task<Result<task::TaskVariables>> {
 484        let mut vars = task::TaskVariables::default();
 485
 486        if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
 487            vars.insert(
 488                TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
 489                replace_test_name_parameters(symbol),
 490            );
 491            vars.insert(
 492                TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
 493                replace_test_name_parameters(symbol),
 494            );
 495            vars.insert(
 496                TYPESCRIPT_BUN_TEST_NAME_VARIABLE,
 497                replace_test_name_parameters(symbol),
 498            );
 499        }
 500        let file_path = location
 501            .file_location
 502            .buffer
 503            .read(cx)
 504            .file()
 505            .map(|file| file.path());
 506
 507        let args = location.worktree_root.zip(location.fs).zip(file_path).map(
 508            |((worktree_root, fs), file_path)| {
 509                (
 510                    self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
 511                    worktree_root,
 512                    fs,
 513                )
 514            },
 515        );
 516        cx.background_spawn(async move {
 517            if let Some((task, worktree_root, fs)) = args {
 518                let package_json_data = task.await.log_err();
 519                vars.insert(
 520                    TYPESCRIPT_RUNNER_VARIABLE,
 521                    detect_package_manager(worktree_root, fs, package_json_data.clone())
 522                        .await
 523                        .to_owned(),
 524                );
 525
 526                if let Some(package_json_data) = package_json_data {
 527                    if let Some(path) = package_json_data.jest_package_path {
 528                        vars.insert(
 529                            TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
 530                            path.parent()
 531                                .unwrap_or(Path::new(""))
 532                                .to_string_lossy()
 533                                .to_string(),
 534                        );
 535                    }
 536
 537                    if let Some(path) = package_json_data.mocha_package_path {
 538                        vars.insert(
 539                            TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
 540                            path.parent()
 541                                .unwrap_or(Path::new(""))
 542                                .to_string_lossy()
 543                                .to_string(),
 544                        );
 545                    }
 546
 547                    if let Some(path) = package_json_data.vitest_package_path {
 548                        vars.insert(
 549                            TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
 550                            path.parent()
 551                                .unwrap_or(Path::new(""))
 552                                .to_string_lossy()
 553                                .to_string(),
 554                        );
 555                    }
 556
 557                    if let Some(path) = package_json_data.jasmine_package_path {
 558                        vars.insert(
 559                            TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
 560                            path.parent()
 561                                .unwrap_or(Path::new(""))
 562                                .to_string_lossy()
 563                                .to_string(),
 564                        );
 565                    }
 566
 567                    if let Some(path) = package_json_data.bun_package_path {
 568                        vars.insert(
 569                            TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE,
 570                            path.parent()
 571                                .unwrap_or(Path::new(""))
 572                                .to_string_lossy()
 573                                .to_string(),
 574                        );
 575                    }
 576
 577                    if let Some(path) = package_json_data.node_package_path {
 578                        vars.insert(
 579                            TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE,
 580                            path.parent()
 581                                .unwrap_or(Path::new(""))
 582                                .to_string_lossy()
 583                                .to_string(),
 584                        );
 585                    }
 586                }
 587            }
 588            Ok(vars)
 589        })
 590    }
 591}
 592
 593fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 594    vec![server_path.into(), "--stdio".into()]
 595}
 596
 597fn replace_test_name_parameters(test_name: &str) -> String {
 598    static PATTERN: LazyLock<regex::Regex> =
 599        LazyLock::new(|| regex::Regex::new(r"(\$([A-Za-z0-9_\.]+|[\#])|%[psdifjo#\$%])").unwrap());
 600    PATTERN.split(test_name).map(regex::escape).join("(.+?)")
 601}
 602
 603pub struct TypeScriptLspAdapter {
 604    fs: Arc<dyn Fs>,
 605    node: NodeRuntime,
 606}
 607
 608impl TypeScriptLspAdapter {
 609    const OLD_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.js";
 610    const NEW_SERVER_PATH: &str = "node_modules/typescript-language-server/lib/cli.mjs";
 611
 612    const PACKAGE_NAME: &str = "typescript";
 613    const SERVER_PACKAGE_NAME: &str = "typescript-language-server";
 614
 615    const SERVER_NAME: LanguageServerName =
 616        LanguageServerName::new_static(Self::SERVER_PACKAGE_NAME);
 617
 618    pub fn new(node: NodeRuntime, fs: Arc<dyn Fs>) -> Self {
 619        TypeScriptLspAdapter { fs, node }
 620    }
 621
 622    async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
 623        let is_yarn = adapter
 624            .read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap())
 625            .await
 626            .is_ok();
 627
 628        let tsdk_path = if is_yarn {
 629            ".yarn/sdks/typescript/lib"
 630        } else {
 631            "node_modules/typescript/lib"
 632        };
 633
 634        if self
 635            .fs
 636            .is_dir(&adapter.worktree_root_path().join(tsdk_path))
 637            .await
 638        {
 639            Some(tsdk_path)
 640        } else {
 641            None
 642        }
 643    }
 644}
 645
 646pub struct TypeScriptVersions {
 647    typescript_version: Version,
 648    server_version: Version,
 649}
 650
 651impl LspInstaller for TypeScriptLspAdapter {
 652    type BinaryVersion = TypeScriptVersions;
 653
 654    async fn fetch_latest_server_version(
 655        &self,
 656        _: &dyn LspAdapterDelegate,
 657        _: bool,
 658        _: &mut AsyncApp,
 659    ) -> Result<Self::BinaryVersion> {
 660        Ok(TypeScriptVersions {
 661            typescript_version: self
 662                .node
 663                .npm_package_latest_version(Self::PACKAGE_NAME)
 664                .await?,
 665            server_version: self
 666                .node
 667                .npm_package_latest_version(Self::SERVER_PACKAGE_NAME)
 668                .await?,
 669        })
 670    }
 671
 672    async fn check_if_version_installed(
 673        &self,
 674        version: &Self::BinaryVersion,
 675        container_dir: &PathBuf,
 676        _: &dyn LspAdapterDelegate,
 677    ) -> Option<LanguageServerBinary> {
 678        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 679
 680        if self
 681            .node
 682            .should_install_npm_package(
 683                Self::PACKAGE_NAME,
 684                &server_path,
 685                container_dir,
 686                VersionStrategy::Latest(&version.typescript_version),
 687            )
 688            .await
 689        {
 690            return None;
 691        }
 692
 693        if self
 694            .node
 695            .should_install_npm_package(
 696                Self::SERVER_PACKAGE_NAME,
 697                &server_path,
 698                container_dir,
 699                VersionStrategy::Latest(&version.server_version),
 700            )
 701            .await
 702        {
 703            return None;
 704        }
 705
 706        Some(LanguageServerBinary {
 707            path: self.node.binary_path().await.ok()?,
 708            env: None,
 709            arguments: typescript_server_binary_arguments(&server_path),
 710        })
 711    }
 712
 713    async fn fetch_server_binary(
 714        &self,
 715        latest_version: Self::BinaryVersion,
 716        container_dir: PathBuf,
 717        _: &dyn LspAdapterDelegate,
 718    ) -> Result<LanguageServerBinary> {
 719        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 720
 721        self.node
 722            .npm_install_packages(
 723                &container_dir,
 724                &[
 725                    (
 726                        Self::PACKAGE_NAME,
 727                        &latest_version.typescript_version.to_string(),
 728                    ),
 729                    (
 730                        Self::SERVER_PACKAGE_NAME,
 731                        &latest_version.server_version.to_string(),
 732                    ),
 733                ],
 734            )
 735            .await?;
 736
 737        Ok(LanguageServerBinary {
 738            path: self.node.binary_path().await?,
 739            env: None,
 740            arguments: typescript_server_binary_arguments(&server_path),
 741        })
 742    }
 743
 744    async fn cached_server_binary(
 745        &self,
 746        container_dir: PathBuf,
 747        _: &dyn LspAdapterDelegate,
 748    ) -> Option<LanguageServerBinary> {
 749        get_cached_ts_server_binary(container_dir, &self.node).await
 750    }
 751}
 752
 753#[async_trait(?Send)]
 754impl LspAdapter for TypeScriptLspAdapter {
 755    fn name(&self) -> LanguageServerName {
 756        Self::SERVER_NAME
 757    }
 758
 759    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
 760        Some(vec![
 761            CodeActionKind::QUICKFIX,
 762            CodeActionKind::REFACTOR,
 763            CodeActionKind::REFACTOR_EXTRACT,
 764            CodeActionKind::SOURCE,
 765        ])
 766    }
 767
 768    async fn label_for_completion(
 769        &self,
 770        item: &lsp::CompletionItem,
 771        language: &Arc<language::Language>,
 772    ) -> Option<language::CodeLabel> {
 773        use lsp::CompletionItemKind as Kind;
 774        let label_len = item.label.len();
 775        let grammar = language.grammar()?;
 776        let highlight_id = match item.kind? {
 777            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
 778            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
 779            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
 780            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
 781            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
 782            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
 783            _ => None,
 784        }?;
 785
 786        let text = if let Some(description) = item
 787            .label_details
 788            .as_ref()
 789            .and_then(|label_details| label_details.description.as_ref())
 790        {
 791            format!("{} {}", item.label, description)
 792        } else if let Some(detail) = &item.detail {
 793            format!("{} {}", item.label, detail)
 794        } else {
 795            item.label.clone()
 796        };
 797        Some(language::CodeLabel::filtered(
 798            text,
 799            label_len,
 800            item.filter_text.as_deref(),
 801            vec![(0..label_len, highlight_id)],
 802        ))
 803    }
 804
 805    async fn initialization_options(
 806        self: Arc<Self>,
 807        adapter: &Arc<dyn LspAdapterDelegate>,
 808        _: &mut AsyncApp,
 809    ) -> Result<Option<serde_json::Value>> {
 810        let tsdk_path = self.tsdk_path(adapter).await;
 811        Ok(Some(json!({
 812            "provideFormatter": true,
 813            "hostInfo": "zed",
 814            "tsserver": {
 815                "path": tsdk_path,
 816            },
 817            "preferences": {
 818                "includeInlayParameterNameHints": "all",
 819                "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
 820                "includeInlayFunctionParameterTypeHints": true,
 821                "includeInlayVariableTypeHints": true,
 822                "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
 823                "includeInlayPropertyDeclarationTypeHints": true,
 824                "includeInlayFunctionLikeReturnTypeHints": true,
 825                "includeInlayEnumMemberValueHints": true,
 826            }
 827        })))
 828    }
 829
 830    async fn workspace_configuration(
 831        self: Arc<Self>,
 832        delegate: &Arc<dyn LspAdapterDelegate>,
 833        _: Option<Toolchain>,
 834        _: Option<Uri>,
 835        cx: &mut AsyncApp,
 836    ) -> Result<Value> {
 837        let override_options = cx.update(|cx| {
 838            language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
 839                .and_then(|s| s.settings.clone())
 840        });
 841        if let Some(options) = override_options {
 842            return Ok(options);
 843        }
 844        Ok(json!({
 845            "completions": {
 846              "completeFunctionCalls": true
 847            }
 848        }))
 849    }
 850
 851    fn language_ids(&self) -> HashMap<LanguageName, String> {
 852        HashMap::from_iter([
 853            (LanguageName::new_static("TypeScript"), "typescript".into()),
 854            (LanguageName::new_static("JavaScript"), "javascript".into()),
 855            (LanguageName::new_static("TSX"), "typescriptreact".into()),
 856        ])
 857    }
 858}
 859
 860async fn get_cached_ts_server_binary(
 861    container_dir: PathBuf,
 862    node: &NodeRuntime,
 863) -> Option<LanguageServerBinary> {
 864    maybe!(async {
 865        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
 866        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
 867        if new_server_path.exists() {
 868            Ok(LanguageServerBinary {
 869                path: node.binary_path().await?,
 870                env: None,
 871                arguments: typescript_server_binary_arguments(&new_server_path),
 872            })
 873        } else if old_server_path.exists() {
 874            Ok(LanguageServerBinary {
 875                path: node.binary_path().await?,
 876                env: None,
 877                arguments: typescript_server_binary_arguments(&old_server_path),
 878            })
 879        } else {
 880            anyhow::bail!("missing executable in directory {container_dir:?}")
 881        }
 882    })
 883    .await
 884    .log_err()
 885}
 886
 887#[cfg(test)]
 888mod tests {
 889    use std::path::Path;
 890
 891    use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
 892    use project::FakeFs;
 893    use serde_json::json;
 894    use task::TaskTemplates;
 895    use unindent::Unindent;
 896    use util::{path, rel_path::rel_path};
 897
 898    use crate::typescript::{
 899        PackageJsonData, TypeScriptContextProvider, replace_test_name_parameters,
 900    };
 901
 902    #[gpui::test]
 903    async fn test_outline(cx: &mut TestAppContext) {
 904        for language in [
 905            crate::language(
 906                "typescript",
 907                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
 908            ),
 909            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
 910        ] {
 911            let text = r#"
 912            function a() {
 913              // local variables are included
 914              let a1 = 1;
 915              // all functions are included
 916              async function a2() {}
 917            }
 918            // top-level variables are included
 919            let b: C
 920            function getB() {}
 921            // exported variables are included
 922            export const d = e;
 923        "#
 924            .unindent();
 925
 926            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
 927            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
 928            assert_eq!(
 929                outline
 930                    .items
 931                    .iter()
 932                    .map(|item| (item.text.as_str(), item.depth))
 933                    .collect::<Vec<_>>(),
 934                &[
 935                    ("function a()", 0),
 936                    ("let a1", 1),
 937                    ("async function a2()", 1),
 938                    ("let b", 0),
 939                    ("function getB()", 0),
 940                    ("const d", 0),
 941                ]
 942            );
 943        }
 944    }
 945
 946    #[gpui::test]
 947    async fn test_outline_with_destructuring(cx: &mut TestAppContext) {
 948        for language in [
 949            crate::language(
 950                "typescript",
 951                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
 952            ),
 953            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
 954        ] {
 955            let text = r#"
 956            // Top-level destructuring
 957            const { a1, a2 } = a;
 958            const [b1, b2] = b;
 959
 960            // Defaults and rest
 961            const [c1 = 1, , c2, ...rest1] = c;
 962            const { d1, d2: e1, f1 = 2, g1: h1 = 3, ...rest2 } = d;
 963
 964            function processData() {
 965              // Nested object destructuring
 966              const { c1, c2 } = c;
 967              // Nested array destructuring
 968              const [d1, d2, d3] = d;
 969              // Destructuring with renaming
 970              const { f1: g1 } = f;
 971              // With defaults
 972              const [x = 10, y] = xy;
 973            }
 974
 975            class DataHandler {
 976              method() {
 977                // Destructuring in class method
 978                const { a1, a2 } = a;
 979                const [b1, ...b2] = b;
 980              }
 981            }
 982        "#
 983            .unindent();
 984
 985            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
 986            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
 987            assert_eq!(
 988                outline
 989                    .items
 990                    .iter()
 991                    .map(|item| (item.text.as_str(), item.depth))
 992                    .collect::<Vec<_>>(),
 993                &[
 994                    ("const a1", 0),
 995                    ("const a2", 0),
 996                    ("const b1", 0),
 997                    ("const b2", 0),
 998                    ("const c1", 0),
 999                    ("const c2", 0),
1000                    ("const rest1", 0),
1001                    ("const d1", 0),
1002                    ("const e1", 0),
1003                    ("const h1", 0),
1004                    ("const rest2", 0),
1005                    ("function processData()", 0),
1006                    ("const c1", 1),
1007                    ("const c2", 1),
1008                    ("const d1", 1),
1009                    ("const d2", 1),
1010                    ("const d3", 1),
1011                    ("const g1", 1),
1012                    ("const x", 1),
1013                    ("const y", 1),
1014                    ("class DataHandler", 0),
1015                    ("method()", 1),
1016                    ("const a1", 2),
1017                    ("const a2", 2),
1018                    ("const b1", 2),
1019                    ("const b2", 2),
1020                ]
1021            );
1022        }
1023    }
1024
1025    #[gpui::test]
1026    async fn test_outline_with_object_properties(cx: &mut TestAppContext) {
1027        for language in [
1028            crate::language(
1029                "typescript",
1030                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1031            ),
1032            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1033        ] {
1034            let text = r#"
1035            // Object with function properties
1036            const o = { m() {}, async n() {}, g: function* () {}, h: () => {}, k: function () {} };
1037
1038            // Object with primitive properties
1039            const p = { p1: 1, p2: "hello", p3: true };
1040
1041            // Nested objects
1042            const q = {
1043                r: {
1044                    // won't be included due to one-level depth limit
1045                    s: 1
1046                },
1047                t: 2
1048            };
1049
1050            function getData() {
1051                const local = { x: 1, y: 2 };
1052                return local;
1053            }
1054        "#
1055            .unindent();
1056
1057            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1058            cx.run_until_parked();
1059            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1060            assert_eq!(
1061                outline
1062                    .items
1063                    .iter()
1064                    .map(|item| (item.text.as_str(), item.depth))
1065                    .collect::<Vec<_>>(),
1066                &[
1067                    ("const o", 0),
1068                    ("m()", 1),
1069                    ("async n()", 1),
1070                    ("g", 1),
1071                    ("h", 1),
1072                    ("k", 1),
1073                    ("const p", 0),
1074                    ("p1", 1),
1075                    ("p2", 1),
1076                    ("p3", 1),
1077                    ("const q", 0),
1078                    ("r", 1),
1079                    ("s", 2),
1080                    ("t", 1),
1081                    ("function getData()", 0),
1082                    ("const local", 1),
1083                    ("x", 2),
1084                    ("y", 2),
1085                ]
1086            );
1087        }
1088    }
1089
1090    #[gpui::test]
1091    async fn test_outline_with_nested_object_methods(cx: &mut TestAppContext) {
1092        for language in [
1093            crate::language(
1094                "typescript",
1095                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1096            ),
1097            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1098            crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()),
1099        ] {
1100            let text = r#"
1101            // Reproduction from https://github.com/zed-industries/zed/issues/48711
1102            const a = {
1103              p01: '01',
1104              fn01: () => {},
1105              fn02() {},
1106              deep: {
1107                subFn01: () => {},
1108                subFn02() {},
1109                subP03: '03',
1110                deep2: {
1111                  subFn01: () => {},
1112                  subFn02() {},
1113                  subP03: '03',
1114                },
1115              },
1116            };
1117
1118            // Edge case: async methods in nested objects
1119            const b = {
1120              async topAsync() {},
1121              nested: { async nestedAsync() {} },
1122            };
1123
1124            // Edge case: object literal in function argument
1125            foo({ bar() {}, inner: { baz() {} } });
1126        "#
1127            .unindent();
1128
1129            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1130            cx.run_until_parked();
1131            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1132
1133            let items: Vec<_> = outline
1134                .items
1135                .iter()
1136                .map(|item| (item.text.as_str(), item.depth))
1137                .collect();
1138
1139            assert_eq!(
1140                items,
1141                &[
1142                    ("const a", 0),
1143                    ("p01", 1),
1144                    ("fn01", 1),
1145                    ("fn02()", 1),
1146                    ("deep", 1),
1147                    ("subFn01", 2),
1148                    ("subFn02()", 2),
1149                    ("subP03", 2),
1150                    ("deep2", 2),
1151                    ("subFn01", 3),
1152                    ("subFn02()", 3),
1153                    ("subP03", 3),
1154                    ("const b", 0),
1155                    ("async topAsync()", 1),
1156                    ("nested", 1),
1157                    ("async nestedAsync()", 2),
1158                    ("bar()", 0),
1159                    ("inner", 0),
1160                    ("baz()", 1),
1161                ]
1162            );
1163        }
1164    }
1165
1166    #[gpui::test]
1167    async fn test_outline_with_complex_nested_objects(cx: &mut TestAppContext) {
1168        for language in [
1169            crate::language(
1170                "typescript",
1171                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1172            ),
1173            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1174            crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()),
1175        ] {
1176            let text = r#"
1177            const config = {
1178              init() {},
1179              destroy() {},
1180              api: {
1181                baseUrl: "x",
1182                fetchData() {},
1183                async submitForm() {},
1184                errorHandler() {},
1185              },
1186              features: {
1187                auth: {
1188                  login() {},
1189                  logout() {},
1190                  refreshToken() {},
1191                },
1192                cache: {
1193                  get() {},
1194                  set() {},
1195                  invalidate() {},
1196                },
1197              },
1198              watch: {
1199                value() {},
1200              },
1201              computed: {
1202                fullName() {},
1203                displayValue() {},
1204              },
1205            };
1206
1207            registerPlugin({
1208              name: "my-plugin",
1209              setup() {},
1210              teardown() {},
1211              hooks: {
1212                beforeMount() {},
1213                mounted() {},
1214                beforeUnmount() {},
1215              },
1216            });
1217
1218            export const store = {
1219              state: {},
1220              mutations: {
1221                setUser() {},
1222                clearUser() {},
1223              },
1224              actions: {
1225                async fetchUser() {},
1226                logout() {},
1227              },
1228              getters: {
1229                currentUser() {},
1230                isAuthenticated() {},
1231              },
1232            };
1233
1234            function registerPlugin(_plugin: unknown) {}
1235        "#
1236            .unindent();
1237
1238            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1239            cx.run_until_parked();
1240            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1241
1242            let items: Vec<_> = outline
1243                .items
1244                .iter()
1245                .map(|item| (item.text.as_str(), item.depth))
1246                .collect();
1247
1248            assert_eq!(
1249                items,
1250                &[
1251                    ("const config", 0),
1252                    ("init()", 1),
1253                    ("destroy()", 1),
1254                    ("api", 1),
1255                    ("baseUrl", 2),
1256                    ("fetchData()", 2),
1257                    ("async submitForm()", 2),
1258                    ("errorHandler()", 2),
1259                    ("features", 1),
1260                    ("auth", 2),
1261                    ("login()", 3),
1262                    ("logout()", 3),
1263                    ("refreshToken()", 3),
1264                    ("cache", 2),
1265                    ("get()", 3),
1266                    ("set()", 3),
1267                    ("invalidate()", 3),
1268                    ("watch", 1),
1269                    ("value()", 2),
1270                    ("computed", 1),
1271                    ("fullName()", 2),
1272                    ("displayValue()", 2),
1273                    ("name", 0),
1274                    ("setup()", 0),
1275                    ("teardown()", 0),
1276                    ("hooks", 0),
1277                    ("beforeMount()", 1),
1278                    ("mounted()", 1),
1279                    ("beforeUnmount()", 1),
1280                    ("const store", 0),
1281                    ("state", 1),
1282                    ("mutations", 1),
1283                    ("setUser()", 2),
1284                    ("clearUser()", 2),
1285                    ("actions", 1),
1286                    ("async fetchUser()", 2),
1287                    ("logout()", 2),
1288                    ("getters", 1),
1289                    ("currentUser()", 2),
1290                    ("isAuthenticated()", 2),
1291                    ("function registerPlugin( )", 0),
1292                ]
1293            );
1294        }
1295    }
1296
1297    #[gpui::test]
1298    async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) {
1299        for language in [
1300            crate::language(
1301                "typescript",
1302                tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1303            ),
1304            crate::language("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
1305        ] {
1306            let text = r#"
1307            // Symbols as object keys
1308            const sym = Symbol("test");
1309            const obj1 = {
1310                [sym]: 1,
1311                [Symbol("inline")]: 2,
1312                normalKey: 3
1313            };
1314
1315            // Enums as object keys
1316            enum Color { Red, Blue, Green }
1317
1318            const obj2 = {
1319                [Color.Red]: "red value",
1320                [Color.Blue]: "blue value",
1321                regularProp: "normal"
1322            };
1323
1324            // Mixed computed properties
1325            const key = "dynamic";
1326            const obj3 = {
1327                [key]: 1,
1328                ["string" + "concat"]: 2,
1329                [1 + 1]: 3,
1330                static: 4
1331            };
1332
1333            // Nested objects with computed properties
1334            const obj4 = {
1335                [sym]: {
1336                    nested: 1
1337                },
1338                regular: {
1339                    [key]: 2
1340                }
1341            };
1342        "#
1343            .unindent();
1344
1345            let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1346            let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1347            assert_eq!(
1348                outline
1349                    .items
1350                    .iter()
1351                    .map(|item| (item.text.as_str(), item.depth))
1352                    .collect::<Vec<_>>(),
1353                &[
1354                    ("const sym", 0),
1355                    ("const obj1", 0),
1356                    ("[sym]", 1),
1357                    ("[Symbol(\"inline\")]", 1),
1358                    ("normalKey", 1),
1359                    ("enum Color", 0),
1360                    ("const obj2", 0),
1361                    ("[Color.Red]", 1),
1362                    ("[Color.Blue]", 1),
1363                    ("regularProp", 1),
1364                    ("const key", 0),
1365                    ("const obj3", 0),
1366                    ("[key]", 1),
1367                    ("[\"string\" + \"concat\"]", 1),
1368                    ("[1 + 1]", 1),
1369                    ("static", 1),
1370                    ("const obj4", 0),
1371                    ("[sym]", 1),
1372                    ("nested", 2),
1373                    ("regular", 1),
1374                    ("[key]", 2),
1375                ]
1376            );
1377        }
1378    }
1379
1380    #[gpui::test]
1381    async fn test_generator_function_outline(cx: &mut TestAppContext) {
1382        let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
1383
1384        let text = r#"
1385            function normalFunction() {
1386                console.log("normal");
1387            }
1388
1389            function* simpleGenerator() {
1390                yield 1;
1391                yield 2;
1392            }
1393
1394            async function* asyncGenerator() {
1395                yield await Promise.resolve(1);
1396            }
1397
1398            function* generatorWithParams(start, end) {
1399                for (let i = start; i <= end; i++) {
1400                    yield i;
1401                }
1402            }
1403
1404            class TestClass {
1405                *methodGenerator() {
1406                    yield "method";
1407                }
1408
1409                async *asyncMethodGenerator() {
1410                    yield "async method";
1411                }
1412            }
1413        "#
1414        .unindent();
1415
1416        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1417        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
1418        assert_eq!(
1419            outline
1420                .items
1421                .iter()
1422                .map(|item| (item.text.as_str(), item.depth))
1423                .collect::<Vec<_>>(),
1424            &[
1425                ("function normalFunction()", 0),
1426                ("function* simpleGenerator()", 0),
1427                ("async function* asyncGenerator()", 0),
1428                ("function* generatorWithParams( )", 0),
1429                ("class TestClass", 0),
1430                ("*methodGenerator()", 1),
1431                ("async *asyncMethodGenerator()", 1),
1432            ]
1433        );
1434    }
1435
1436    #[gpui::test]
1437    async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1438        cx.update(|cx| {
1439            settings::init(cx);
1440        });
1441
1442        let package_json_1 = json!({
1443            "dependencies": {
1444                "mocha": "1.0.0",
1445                "vitest": "1.0.0"
1446            },
1447            "scripts": {
1448                "test": ""
1449            }
1450        })
1451        .to_string();
1452
1453        let package_json_2 = json!({
1454            "devDependencies": {
1455                "vitest": "2.0.0"
1456            },
1457            "scripts": {
1458                "test": ""
1459            }
1460        })
1461        .to_string();
1462
1463        let fs = FakeFs::new(executor);
1464        fs.insert_tree(
1465            path!("/root"),
1466            json!({
1467                "package.json": package_json_1,
1468                "sub": {
1469                    "package.json": package_json_2,
1470                    "file.js": "",
1471                }
1472            }),
1473        )
1474        .await;
1475
1476        let provider = TypeScriptContextProvider::new(fs.clone());
1477        let package_json_data = cx
1478            .update(|cx| {
1479                provider.combined_package_json_data(
1480                    fs.clone(),
1481                    path!("/root").as_ref(),
1482                    rel_path("sub/file1.js"),
1483                    cx,
1484                )
1485            })
1486            .await
1487            .unwrap();
1488        pretty_assertions::assert_eq!(
1489            package_json_data,
1490            PackageJsonData {
1491                jest_package_path: None,
1492                mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
1493                vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
1494                jasmine_package_path: None,
1495                bun_package_path: None,
1496                node_package_path: None,
1497                scripts: [
1498                    (
1499                        Path::new(path!("/root/package.json")).into(),
1500                        "test".to_owned()
1501                    ),
1502                    (
1503                        Path::new(path!("/root/sub/package.json")).into(),
1504                        "test".to_owned()
1505                    )
1506                ]
1507                .into_iter()
1508                .collect(),
1509                package_manager: None,
1510            }
1511        );
1512
1513        let mut task_templates = TaskTemplates::default();
1514        package_json_data.fill_task_templates(&mut task_templates);
1515        let task_templates = task_templates
1516            .0
1517            .into_iter()
1518            .map(|template| (template.label, template.cwd))
1519            .collect::<Vec<_>>();
1520        pretty_assertions::assert_eq!(
1521            task_templates,
1522            [
1523                (
1524                    "vitest file test".into(),
1525                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1526                ),
1527                (
1528                    "vitest test $ZED_SYMBOL".into(),
1529                    Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()),
1530                ),
1531                (
1532                    "mocha file test".into(),
1533                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1534                ),
1535                (
1536                    "mocha test $ZED_SYMBOL".into(),
1537                    Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()),
1538                ),
1539                (
1540                    "root/package.json > test".into(),
1541                    Some(path!("/root").into())
1542                ),
1543                (
1544                    "sub/package.json > test".into(),
1545                    Some(path!("/root/sub").into())
1546                ),
1547            ]
1548        );
1549    }
1550
1551    #[test]
1552    fn test_escaping_name() {
1553        let cases = [
1554            ("plain test name", "plain test name"),
1555            ("test name with $param_name", "test name with (.+?)"),
1556            ("test name with $nested.param.name", "test name with (.+?)"),
1557            ("test name with $#", "test name with (.+?)"),
1558            ("test name with $##", "test name with (.+?)\\#"),
1559            ("test name with %p", "test name with (.+?)"),
1560            ("test name with %s", "test name with (.+?)"),
1561            ("test name with %d", "test name with (.+?)"),
1562            ("test name with %i", "test name with (.+?)"),
1563            ("test name with %f", "test name with (.+?)"),
1564            ("test name with %j", "test name with (.+?)"),
1565            ("test name with %o", "test name with (.+?)"),
1566            ("test name with %#", "test name with (.+?)"),
1567            ("test name with %$", "test name with (.+?)"),
1568            ("test name with %%", "test name with (.+?)"),
1569            ("test name with %q", "test name with %q"),
1570            (
1571                "test name with regex chars .*+?^${}()|[]\\",
1572                "test name with regex chars \\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\",
1573            ),
1574            (
1575                "test name with multiple $params and %pretty and %b and (.+?)",
1576                "test name with multiple (.+?) and (.+?)retty and %b and \\(\\.\\+\\?\\)",
1577            ),
1578        ];
1579
1580        for (input, expected) in cases {
1581            assert_eq!(replace_test_name_parameters(input), expected);
1582        }
1583    }
1584
1585    // The order of test runner tasks is based on inferred user preference:
1586    // 1. Dedicated test runners (e.g., Jest, Vitest, Mocha, Jasmine) are prioritized.
1587    // 2. Bun's built-in test runner (`bun test`) comes next.
1588    // 3. Node.js's built-in test runner (`node --test`) is last.
1589    // This hierarchy assumes that if a dedicated test framework is installed, it is the
1590    // preferred testing mechanism. Between runtime-specific options, `bun test` is
1591    // typically preferred over `node --test` when @types/bun is present.
1592    #[gpui::test]
1593    async fn test_task_ordering_with_multiple_test_runners(
1594        executor: BackgroundExecutor,
1595        cx: &mut TestAppContext,
1596    ) {
1597        cx.update(|cx| {
1598            settings::init(cx);
1599        });
1600
1601        // Test case with all test runners present
1602        let package_json_all_runners = json!({
1603            "devDependencies": {
1604                "@types/bun": "1.0.0",
1605                "@types/node": "^20.0.0",
1606                "jest": "29.0.0",
1607                "mocha": "10.0.0",
1608                "vitest": "1.0.0",
1609                "jasmine": "5.0.0",
1610            },
1611            "scripts": {
1612                "test": "jest"
1613            }
1614        })
1615        .to_string();
1616
1617        let fs = FakeFs::new(executor);
1618        fs.insert_tree(
1619            path!("/root"),
1620            json!({
1621                "package.json": package_json_all_runners,
1622                "file.js": "",
1623            }),
1624        )
1625        .await;
1626
1627        let provider = TypeScriptContextProvider::new(fs.clone());
1628
1629        let package_json_data = cx
1630            .update(|cx| {
1631                provider.combined_package_json_data(
1632                    fs.clone(),
1633                    path!("/root").as_ref(),
1634                    rel_path("file.js"),
1635                    cx,
1636                )
1637            })
1638            .await
1639            .unwrap();
1640
1641        assert!(package_json_data.jest_package_path.is_some());
1642        assert!(package_json_data.mocha_package_path.is_some());
1643        assert!(package_json_data.vitest_package_path.is_some());
1644        assert!(package_json_data.jasmine_package_path.is_some());
1645        assert!(package_json_data.bun_package_path.is_some());
1646        assert!(package_json_data.node_package_path.is_some());
1647
1648        let mut task_templates = TaskTemplates::default();
1649        package_json_data.fill_task_templates(&mut task_templates);
1650
1651        let test_tasks: Vec<_> = task_templates
1652            .0
1653            .iter()
1654            .filter(|template| {
1655                template.tags.contains(&"ts-test".to_owned())
1656                    || template.tags.contains(&"js-test".to_owned())
1657            })
1658            .map(|template| &template.label)
1659            .collect();
1660
1661        let node_test_index = test_tasks
1662            .iter()
1663            .position(|label| label.contains("node test"));
1664        let jest_test_index = test_tasks.iter().position(|label| label.contains("jest"));
1665        let bun_test_index = test_tasks
1666            .iter()
1667            .position(|label| label.contains("bun test"));
1668
1669        assert!(
1670            node_test_index.is_some(),
1671            "Node test tasks should be present"
1672        );
1673        assert!(
1674            jest_test_index.is_some(),
1675            "Jest test tasks should be present"
1676        );
1677        assert!(bun_test_index.is_some(), "Bun test tasks should be present");
1678
1679        assert!(
1680            jest_test_index.unwrap() < bun_test_index.unwrap(),
1681            "Jest should come before Bun"
1682        );
1683        assert!(
1684            bun_test_index.unwrap() < node_test_index.unwrap(),
1685            "Bun should come before Node"
1686        );
1687    }
1688}