typescript.rs

   1use anyhow::{Context as _, Result};
   2use async_compression::futures::bufread::GzipDecoder;
   3use async_tar::Archive;
   4use async_trait::async_trait;
   5use chrono::{DateTime, Local};
   6use collections::HashMap;
   7use futures::future::join_all;
   8use gpui::{App, AppContext, AsyncApp, Task};
   9use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
  10use language::{
  11    ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
  12};
  13use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
  14use node_runtime::NodeRuntime;
  15use project::{Fs, lsp_store::language_server_settings};
  16use serde_json::{Value, json};
  17use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
  18use std::{
  19    any::Any,
  20    borrow::Cow,
  21    collections::BTreeSet,
  22    ffi::OsString,
  23    path::{Path, PathBuf},
  24    sync::Arc,
  25};
  26use task::{TaskTemplate, TaskTemplates, VariableName};
  27use util::archive::extract_zip;
  28use util::merge_json_value_into;
  29use util::{ResultExt, fs::remove_matching, maybe};
  30
  31#[derive(Debug)]
  32pub(crate) struct TypeScriptContextProvider {
  33    last_package_json: PackageJsonContents,
  34}
  35
  36const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
  37    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
  38
  39const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
  40    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
  41
  42const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
  43    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
  44
  45const TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE: VariableName =
  46    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_PACKAGE_PATH"));
  47
  48const TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE: VariableName =
  49    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA_PACKAGE_PATH"));
  50
  51const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
  52    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_PACKAGE_PATH"));
  53
  54const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
  55    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
  56
  57#[derive(Clone, Debug, Default)]
  58struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
  59
  60#[derive(Clone, Debug)]
  61struct PackageJson {
  62    mtime: DateTime<Local>,
  63    data: PackageJsonData,
  64}
  65
  66#[derive(Clone, Debug, Default, PartialEq, Eq)]
  67struct PackageJsonData {
  68    jest_package_path: Option<Arc<Path>>,
  69    mocha_package_path: Option<Arc<Path>>,
  70    vitest_package_path: Option<Arc<Path>>,
  71    jasmine_package_path: Option<Arc<Path>>,
  72    scripts: BTreeSet<(Arc<Path>, String)>,
  73    package_manager: Option<&'static str>,
  74}
  75
  76impl PackageJsonData {
  77    fn new(path: Arc<Path>, package_json: HashMap<String, Value>) -> Self {
  78        let mut scripts = BTreeSet::new();
  79        if let Some(serde_json::Value::Object(package_json_scripts)) = package_json.get("scripts") {
  80            scripts.extend(
  81                package_json_scripts
  82                    .keys()
  83                    .cloned()
  84                    .map(|name| (path.clone(), name)),
  85            );
  86        }
  87
  88        let mut jest_package_path = None;
  89        let mut mocha_package_path = None;
  90        let mut vitest_package_path = None;
  91        let mut jasmine_package_path = None;
  92        if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {
  93            if dependencies.contains_key("jest") {
  94                jest_package_path.get_or_insert_with(|| path.clone());
  95            }
  96            if dependencies.contains_key("mocha") {
  97                mocha_package_path.get_or_insert_with(|| path.clone());
  98            }
  99            if dependencies.contains_key("vitest") {
 100                vitest_package_path.get_or_insert_with(|| path.clone());
 101            }
 102            if dependencies.contains_key("jasmine") {
 103                jasmine_package_path.get_or_insert_with(|| path.clone());
 104            }
 105        }
 106        if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")
 107        {
 108            if dev_dependencies.contains_key("jest") {
 109                jest_package_path.get_or_insert_with(|| path.clone());
 110            }
 111            if dev_dependencies.contains_key("mocha") {
 112                mocha_package_path.get_or_insert_with(|| path.clone());
 113            }
 114            if dev_dependencies.contains_key("vitest") {
 115                vitest_package_path.get_or_insert_with(|| path.clone());
 116            }
 117            if dev_dependencies.contains_key("jasmine") {
 118                jasmine_package_path.get_or_insert_with(|| path.clone());
 119            }
 120        }
 121
 122        let package_manager = package_json
 123            .get("packageManager")
 124            .and_then(|value| value.as_str())
 125            .and_then(|value| {
 126                if value.starts_with("pnpm") {
 127                    Some("pnpm")
 128                } else if value.starts_with("yarn") {
 129                    Some("yarn")
 130                } else if value.starts_with("npm") {
 131                    Some("npm")
 132                } else {
 133                    None
 134                }
 135            });
 136
 137        Self {
 138            jest_package_path,
 139            mocha_package_path,
 140            vitest_package_path,
 141            jasmine_package_path,
 142            scripts,
 143            package_manager,
 144        }
 145    }
 146
 147    fn merge(&mut self, other: Self) {
 148        self.jest_package_path = self.jest_package_path.take().or(other.jest_package_path);
 149        self.mocha_package_path = self.mocha_package_path.take().or(other.mocha_package_path);
 150        self.vitest_package_path = self
 151            .vitest_package_path
 152            .take()
 153            .or(other.vitest_package_path);
 154        self.jasmine_package_path = self
 155            .jasmine_package_path
 156            .take()
 157            .or(other.jasmine_package_path);
 158        self.scripts.extend(other.scripts);
 159        self.package_manager = self.package_manager.or(other.package_manager);
 160    }
 161
 162    fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
 163        if self.jest_package_path.is_some() {
 164            task_templates.0.push(TaskTemplate {
 165                label: "jest file test".to_owned(),
 166                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 167                args: vec![
 168                    "exec".to_owned(),
 169                    "--".to_owned(),
 170                    "jest".to_owned(),
 171                    "--runInBand".to_owned(),
 172                    VariableName::File.template_value(),
 173                ],
 174                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
 175                ..TaskTemplate::default()
 176            });
 177            task_templates.0.push(TaskTemplate {
 178                label: format!("jest test {}", VariableName::Symbol.template_value()),
 179                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 180                args: vec![
 181                    "exec".to_owned(),
 182                    "--".to_owned(),
 183                    "jest".to_owned(),
 184                    "--runInBand".to_owned(),
 185                    "--testNamePattern".to_owned(),
 186                    format!(
 187                        "\"{}\"",
 188                        TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
 189                    ),
 190                    VariableName::File.template_value(),
 191                ],
 192                tags: vec![
 193                    "ts-test".to_owned(),
 194                    "js-test".to_owned(),
 195                    "tsx-test".to_owned(),
 196                ],
 197                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
 198                ..TaskTemplate::default()
 199            });
 200        }
 201
 202        if self.vitest_package_path.is_some() {
 203            task_templates.0.push(TaskTemplate {
 204                label: format!("{} file test", "vitest".to_owned()),
 205                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 206                args: vec![
 207                    "exec".to_owned(),
 208                    "--".to_owned(),
 209                    "vitest".to_owned(),
 210                    "run".to_owned(),
 211                    "--poolOptions.forks.minForks=0".to_owned(),
 212                    "--poolOptions.forks.maxForks=1".to_owned(),
 213                    VariableName::File.template_value(),
 214                ],
 215                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
 216                ..TaskTemplate::default()
 217            });
 218            task_templates.0.push(TaskTemplate {
 219                label: format!(
 220                    "{} test {}",
 221                    "vitest".to_owned(),
 222                    VariableName::Symbol.template_value(),
 223                ),
 224                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 225                args: vec![
 226                    "exec".to_owned(),
 227                    "--".to_owned(),
 228                    "vitest".to_owned(),
 229                    "run".to_owned(),
 230                    "--poolOptions.forks.minForks=0".to_owned(),
 231                    "--poolOptions.forks.maxForks=1".to_owned(),
 232                    "--testNamePattern".to_owned(),
 233                    format!(
 234                        "\"{}\"",
 235                        TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()
 236                    ),
 237                    VariableName::File.template_value(),
 238                ],
 239                tags: vec![
 240                    "ts-test".to_owned(),
 241                    "js-test".to_owned(),
 242                    "tsx-test".to_owned(),
 243                ],
 244                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
 245                ..TaskTemplate::default()
 246            });
 247        }
 248
 249        if self.mocha_package_path.is_some() {
 250            task_templates.0.push(TaskTemplate {
 251                label: format!("{} file test", "mocha".to_owned()),
 252                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 253                args: vec![
 254                    "exec".to_owned(),
 255                    "--".to_owned(),
 256                    "mocha".to_owned(),
 257                    VariableName::File.template_value(),
 258                ],
 259                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
 260                ..TaskTemplate::default()
 261            });
 262            task_templates.0.push(TaskTemplate {
 263                label: format!(
 264                    "{} test {}",
 265                    "mocha".to_owned(),
 266                    VariableName::Symbol.template_value(),
 267                ),
 268                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 269                args: vec![
 270                    "exec".to_owned(),
 271                    "--".to_owned(),
 272                    "mocha".to_owned(),
 273                    "--grep".to_owned(),
 274                    format!("\"{}\"", VariableName::Symbol.template_value()),
 275                    VariableName::File.template_value(),
 276                ],
 277                tags: vec![
 278                    "ts-test".to_owned(),
 279                    "js-test".to_owned(),
 280                    "tsx-test".to_owned(),
 281                ],
 282                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
 283                ..TaskTemplate::default()
 284            });
 285        }
 286
 287        if self.jasmine_package_path.is_some() {
 288            task_templates.0.push(TaskTemplate {
 289                label: format!("{} file test", "jasmine".to_owned()),
 290                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 291                args: vec![
 292                    "exec".to_owned(),
 293                    "--".to_owned(),
 294                    "jasmine".to_owned(),
 295                    VariableName::File.template_value(),
 296                ],
 297                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
 298                ..TaskTemplate::default()
 299            });
 300            task_templates.0.push(TaskTemplate {
 301                label: format!(
 302                    "{} test {}",
 303                    "jasmine".to_owned(),
 304                    VariableName::Symbol.template_value(),
 305                ),
 306                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 307                args: vec![
 308                    "exec".to_owned(),
 309                    "--".to_owned(),
 310                    "jasmine".to_owned(),
 311                    format!("--filter={}", VariableName::Symbol.template_value()),
 312                    VariableName::File.template_value(),
 313                ],
 314                tags: vec![
 315                    "ts-test".to_owned(),
 316                    "js-test".to_owned(),
 317                    "tsx-test".to_owned(),
 318                ],
 319                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
 320                ..TaskTemplate::default()
 321            });
 322        }
 323
 324        for (path, script) in &self.scripts {
 325            task_templates.0.push(TaskTemplate {
 326                label: format!("package.json > {script}",),
 327                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
 328                args: vec!["run".to_owned(), script.to_owned()],
 329                tags: vec!["package-script".into()],
 330                cwd: Some(
 331                    path.parent()
 332                        .unwrap_or(Path::new(""))
 333                        .to_string_lossy()
 334                        .to_string(),
 335                ),
 336                ..TaskTemplate::default()
 337            });
 338        }
 339    }
 340}
 341
 342impl TypeScriptContextProvider {
 343    pub fn new() -> Self {
 344        Self {
 345            last_package_json: PackageJsonContents::default(),
 346        }
 347    }
 348
 349    fn combined_package_json_data(
 350        &self,
 351        fs: Arc<dyn Fs>,
 352        worktree_root: &Path,
 353        file_relative_path: &Path,
 354        cx: &App,
 355    ) -> Task<anyhow::Result<PackageJsonData>> {
 356        let new_json_data = file_relative_path
 357            .ancestors()
 358            .map(|path| worktree_root.join(path))
 359            .map(|parent_path| {
 360                self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)
 361            })
 362            .collect::<Vec<_>>();
 363
 364        cx.background_spawn(async move {
 365            let mut package_json_data = PackageJsonData::default();
 366            for new_data in join_all(new_json_data).await.into_iter().flatten() {
 367                package_json_data.merge(new_data);
 368            }
 369            Ok(package_json_data)
 370        })
 371    }
 372
 373    fn package_json_data(
 374        &self,
 375        directory_path: &Path,
 376        existing_package_json: PackageJsonContents,
 377        fs: Arc<dyn Fs>,
 378        cx: &App,
 379    ) -> Task<anyhow::Result<PackageJsonData>> {
 380        let package_json_path = directory_path.join("package.json");
 381        let metadata_check_fs = fs.clone();
 382        cx.background_spawn(async move {
 383            let metadata = metadata_check_fs
 384                .metadata(&package_json_path)
 385                .await
 386                .with_context(|| format!("getting metadata for {package_json_path:?}"))?
 387                .with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
 388            let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
 389            let existing_data = {
 390                let contents = existing_package_json.0.read().await;
 391                contents
 392                    .get(&package_json_path)
 393                    .filter(|package_json| package_json.mtime == mtime)
 394                    .map(|package_json| package_json.data.clone())
 395            };
 396            match existing_data {
 397                Some(existing_data) => Ok(existing_data),
 398                None => {
 399                    let package_json_string =
 400                        fs.load(&package_json_path).await.with_context(|| {
 401                            format!("loading package.json from {package_json_path:?}")
 402                        })?;
 403                    let package_json: HashMap<String, serde_json::Value> =
 404                        serde_json::from_str(&package_json_string).with_context(|| {
 405                            format!("parsing package.json from {package_json_path:?}")
 406                        })?;
 407                    let new_data =
 408                        PackageJsonData::new(package_json_path.as_path().into(), package_json);
 409                    {
 410                        let mut contents = existing_package_json.0.write().await;
 411                        contents.insert(
 412                            package_json_path,
 413                            PackageJson {
 414                                mtime,
 415                                data: new_data.clone(),
 416                            },
 417                        );
 418                    }
 419                    Ok(new_data)
 420                }
 421            }
 422        })
 423    }
 424}
 425
 426async fn detect_package_manager(
 427    worktree_root: PathBuf,
 428    fs: Arc<dyn Fs>,
 429    package_json_data: Option<PackageJsonData>,
 430) -> &'static str {
 431    if let Some(package_json_data) = package_json_data {
 432        if let Some(package_manager) = package_json_data.package_manager {
 433            return package_manager;
 434        }
 435    }
 436    if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
 437        return "pnpm";
 438    }
 439    if fs.is_file(&worktree_root.join("yarn.lock")).await {
 440        return "yarn";
 441    }
 442    "npm"
 443}
 444
 445impl ContextProvider for TypeScriptContextProvider {
 446    fn associated_tasks(
 447        &self,
 448        fs: Arc<dyn Fs>,
 449        file: Option<Arc<dyn File>>,
 450        cx: &App,
 451    ) -> Task<Option<TaskTemplates>> {
 452        let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
 453            return Task::ready(None);
 454        };
 455        let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
 456            return Task::ready(None);
 457        };
 458        let file_relative_path = file.path().clone();
 459        let package_json_data =
 460            self.combined_package_json_data(fs.clone(), &worktree_root, &file_relative_path, cx);
 461
 462        cx.background_spawn(async move {
 463            let mut task_templates = TaskTemplates(Vec::new());
 464            task_templates.0.push(TaskTemplate {
 465                label: format!(
 466                    "execute selection {}",
 467                    VariableName::SelectedText.template_value()
 468                ),
 469                command: "node".to_owned(),
 470                args: vec![
 471                    "-e".to_owned(),
 472                    format!("\"{}\"", VariableName::SelectedText.template_value()),
 473                ],
 474                ..TaskTemplate::default()
 475            });
 476
 477            match package_json_data.await {
 478                Ok(package_json) => {
 479                    package_json.fill_task_templates(&mut task_templates);
 480                }
 481                Err(e) => {
 482                    log::error!(
 483                        "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
 484                    );
 485                }
 486            }
 487
 488            Some(task_templates)
 489        })
 490    }
 491
 492    fn build_context(
 493        &self,
 494        current_vars: &task::TaskVariables,
 495        location: ContextLocation<'_>,
 496        _project_env: Option<HashMap<String, String>>,
 497        _toolchains: Arc<dyn LanguageToolchainStore>,
 498        cx: &mut App,
 499    ) -> Task<Result<task::TaskVariables>> {
 500        let mut vars = task::TaskVariables::default();
 501
 502        if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
 503            vars.insert(
 504                TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
 505                replace_test_name_parameters(symbol),
 506            );
 507            vars.insert(
 508                TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
 509                replace_test_name_parameters(symbol),
 510            );
 511        }
 512        let file_path = location
 513            .file_location
 514            .buffer
 515            .read(cx)
 516            .file()
 517            .map(|file| file.path());
 518
 519        let args = location.worktree_root.zip(location.fs).zip(file_path).map(
 520            |((worktree_root, fs), file_path)| {
 521                (
 522                    self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
 523                    worktree_root,
 524                    fs,
 525                )
 526            },
 527        );
 528        cx.background_spawn(async move {
 529            if let Some((task, worktree_root, fs)) = args {
 530                let package_json_data = task.await.log_err();
 531                vars.insert(
 532                    TYPESCRIPT_RUNNER_VARIABLE,
 533                    detect_package_manager(worktree_root, fs, package_json_data.clone())
 534                        .await
 535                        .to_owned(),
 536                );
 537
 538                if let Some(package_json_data) = package_json_data {
 539                    if let Some(path) = package_json_data.jest_package_path {
 540                        vars.insert(
 541                            TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
 542                            path.parent()
 543                                .unwrap_or(Path::new(""))
 544                                .to_string_lossy()
 545                                .to_string(),
 546                        );
 547                    }
 548
 549                    if let Some(path) = package_json_data.mocha_package_path {
 550                        vars.insert(
 551                            TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
 552                            path.parent()
 553                                .unwrap_or(Path::new(""))
 554                                .to_string_lossy()
 555                                .to_string(),
 556                        );
 557                    }
 558
 559                    if let Some(path) = package_json_data.vitest_package_path {
 560                        vars.insert(
 561                            TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
 562                            path.parent()
 563                                .unwrap_or(Path::new(""))
 564                                .to_string_lossy()
 565                                .to_string(),
 566                        );
 567                    }
 568
 569                    if let Some(path) = package_json_data.jasmine_package_path {
 570                        vars.insert(
 571                            TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
 572                            path.parent()
 573                                .unwrap_or(Path::new(""))
 574                                .to_string_lossy()
 575                                .to_string(),
 576                        );
 577                    }
 578                }
 579            }
 580            Ok(vars)
 581        })
 582    }
 583}
 584
 585fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 586    vec![server_path.into(), "--stdio".into()]
 587}
 588
 589fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 590    vec![
 591        "--max-old-space-size=8192".into(),
 592        server_path.into(),
 593        "--stdio".into(),
 594    ]
 595}
 596
 597fn replace_test_name_parameters(test_name: &str) -> String {
 598    let pattern = regex::Regex::new(r"(%|\$)[0-9a-zA-Z]+").unwrap();
 599
 600    pattern.replace_all(test_name, "(.+?)").to_string()
 601}
 602
 603pub struct TypeScriptLspAdapter {
 604    node: NodeRuntime,
 605}
 606
 607impl TypeScriptLspAdapter {
 608    const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
 609    const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
 610    const SERVER_NAME: LanguageServerName =
 611        LanguageServerName::new_static("typescript-language-server");
 612    const PACKAGE_NAME: &str = "typescript";
 613    pub fn new(node: NodeRuntime) -> Self {
 614        TypeScriptLspAdapter { node }
 615    }
 616    async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
 617        let is_yarn = adapter
 618            .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
 619            .await
 620            .is_ok();
 621
 622        let tsdk_path = if is_yarn {
 623            ".yarn/sdks/typescript/lib"
 624        } else {
 625            "node_modules/typescript/lib"
 626        };
 627
 628        if fs
 629            .is_dir(&adapter.worktree_root_path().join(tsdk_path))
 630            .await
 631        {
 632            Some(tsdk_path)
 633        } else {
 634            None
 635        }
 636    }
 637}
 638
 639struct TypeScriptVersions {
 640    typescript_version: String,
 641    server_version: String,
 642}
 643
 644#[async_trait(?Send)]
 645impl LspAdapter for TypeScriptLspAdapter {
 646    fn name(&self) -> LanguageServerName {
 647        Self::SERVER_NAME.clone()
 648    }
 649
 650    async fn fetch_latest_server_version(
 651        &self,
 652        _: &dyn LspAdapterDelegate,
 653    ) -> Result<Box<dyn 'static + Send + Any>> {
 654        Ok(Box::new(TypeScriptVersions {
 655            typescript_version: self.node.npm_package_latest_version("typescript").await?,
 656            server_version: self
 657                .node
 658                .npm_package_latest_version("typescript-language-server")
 659                .await?,
 660        }) as Box<_>)
 661    }
 662
 663    async fn check_if_version_installed(
 664        &self,
 665        version: &(dyn 'static + Send + Any),
 666        container_dir: &PathBuf,
 667        _: &dyn LspAdapterDelegate,
 668    ) -> Option<LanguageServerBinary> {
 669        let version = version.downcast_ref::<TypeScriptVersions>().unwrap();
 670        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 671
 672        let should_install_language_server = self
 673            .node
 674            .should_install_npm_package(
 675                Self::PACKAGE_NAME,
 676                &server_path,
 677                &container_dir,
 678                version.typescript_version.as_str(),
 679            )
 680            .await;
 681
 682        if should_install_language_server {
 683            None
 684        } else {
 685            Some(LanguageServerBinary {
 686                path: self.node.binary_path().await.ok()?,
 687                env: None,
 688                arguments: typescript_server_binary_arguments(&server_path),
 689            })
 690        }
 691    }
 692
 693    async fn fetch_server_binary(
 694        &self,
 695        latest_version: Box<dyn 'static + Send + Any>,
 696        container_dir: PathBuf,
 697        _: &dyn LspAdapterDelegate,
 698    ) -> Result<LanguageServerBinary> {
 699        let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
 700        let server_path = container_dir.join(Self::NEW_SERVER_PATH);
 701
 702        self.node
 703            .npm_install_packages(
 704                &container_dir,
 705                &[
 706                    (
 707                        Self::PACKAGE_NAME,
 708                        latest_version.typescript_version.as_str(),
 709                    ),
 710                    (
 711                        "typescript-language-server",
 712                        latest_version.server_version.as_str(),
 713                    ),
 714                ],
 715            )
 716            .await?;
 717
 718        Ok(LanguageServerBinary {
 719            path: self.node.binary_path().await?,
 720            env: None,
 721            arguments: typescript_server_binary_arguments(&server_path),
 722        })
 723    }
 724
 725    async fn cached_server_binary(
 726        &self,
 727        container_dir: PathBuf,
 728        _: &dyn LspAdapterDelegate,
 729    ) -> Option<LanguageServerBinary> {
 730        get_cached_ts_server_binary(container_dir, &self.node).await
 731    }
 732
 733    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
 734        Some(vec![
 735            CodeActionKind::QUICKFIX,
 736            CodeActionKind::REFACTOR,
 737            CodeActionKind::REFACTOR_EXTRACT,
 738            CodeActionKind::SOURCE,
 739        ])
 740    }
 741
 742    async fn label_for_completion(
 743        &self,
 744        item: &lsp::CompletionItem,
 745        language: &Arc<language::Language>,
 746    ) -> Option<language::CodeLabel> {
 747        use lsp::CompletionItemKind as Kind;
 748        let len = item.label.len();
 749        let grammar = language.grammar()?;
 750        let highlight_id = match item.kind? {
 751            Kind::CLASS | Kind::INTERFACE | Kind::ENUM => grammar.highlight_id_for_name("type"),
 752            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
 753            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
 754            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
 755            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
 756            Kind::VARIABLE => grammar.highlight_id_for_name("variable"),
 757            _ => None,
 758        }?;
 759
 760        let text = if let Some(description) = item
 761            .label_details
 762            .as_ref()
 763            .and_then(|label_details| label_details.description.as_ref())
 764        {
 765            format!("{} {}", item.label, description)
 766        } else if let Some(detail) = &item.detail {
 767            format!("{} {}", item.label, detail)
 768        } else {
 769            item.label.clone()
 770        };
 771
 772        Some(language::CodeLabel {
 773            text,
 774            runs: vec![(0..len, highlight_id)],
 775            filter_range: 0..len,
 776        })
 777    }
 778
 779    async fn initialization_options(
 780        self: Arc<Self>,
 781        fs: &dyn Fs,
 782        adapter: &Arc<dyn LspAdapterDelegate>,
 783    ) -> Result<Option<serde_json::Value>> {
 784        let tsdk_path = Self::tsdk_path(fs, adapter).await;
 785        Ok(Some(json!({
 786            "provideFormatter": true,
 787            "hostInfo": "zed",
 788            "tsserver": {
 789                "path": tsdk_path,
 790            },
 791            "preferences": {
 792                "includeInlayParameterNameHints": "all",
 793                "includeInlayParameterNameHintsWhenArgumentMatchesName": true,
 794                "includeInlayFunctionParameterTypeHints": true,
 795                "includeInlayVariableTypeHints": true,
 796                "includeInlayVariableTypeHintsWhenTypeMatchesName": true,
 797                "includeInlayPropertyDeclarationTypeHints": true,
 798                "includeInlayFunctionLikeReturnTypeHints": true,
 799                "includeInlayEnumMemberValueHints": true,
 800            }
 801        })))
 802    }
 803
 804    async fn workspace_configuration(
 805        self: Arc<Self>,
 806        _: &dyn Fs,
 807        delegate: &Arc<dyn LspAdapterDelegate>,
 808        _: Arc<dyn LanguageToolchainStore>,
 809        cx: &mut AsyncApp,
 810    ) -> Result<Value> {
 811        let override_options = cx.update(|cx| {
 812            language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
 813                .and_then(|s| s.settings.clone())
 814        })?;
 815        if let Some(options) = override_options {
 816            return Ok(options);
 817        }
 818        Ok(json!({
 819            "completions": {
 820              "completeFunctionCalls": true
 821            }
 822        }))
 823    }
 824
 825    fn language_ids(&self) -> HashMap<String, String> {
 826        HashMap::from_iter([
 827            ("TypeScript".into(), "typescript".into()),
 828            ("JavaScript".into(), "javascript".into()),
 829            ("TSX".into(), "typescriptreact".into()),
 830        ])
 831    }
 832}
 833
 834async fn get_cached_ts_server_binary(
 835    container_dir: PathBuf,
 836    node: &NodeRuntime,
 837) -> Option<LanguageServerBinary> {
 838    maybe!(async {
 839        let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
 840        let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
 841        if new_server_path.exists() {
 842            Ok(LanguageServerBinary {
 843                path: node.binary_path().await?,
 844                env: None,
 845                arguments: typescript_server_binary_arguments(&new_server_path),
 846            })
 847        } else if old_server_path.exists() {
 848            Ok(LanguageServerBinary {
 849                path: node.binary_path().await?,
 850                env: None,
 851                arguments: typescript_server_binary_arguments(&old_server_path),
 852            })
 853        } else {
 854            anyhow::bail!("missing executable in directory {container_dir:?}")
 855        }
 856    })
 857    .await
 858    .log_err()
 859}
 860
 861pub struct EsLintLspAdapter {
 862    node: NodeRuntime,
 863}
 864
 865impl EsLintLspAdapter {
 866    const CURRENT_VERSION: &'static str = "3.0.10";
 867    const CURRENT_VERSION_TAG_NAME: &'static str = "release/3.0.10";
 868
 869    #[cfg(not(windows))]
 870    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
 871    #[cfg(windows)]
 872    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
 873
 874    const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
 875    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("eslint");
 876
 877    const FLAT_CONFIG_FILE_NAMES: &'static [&'static str] = &[
 878        "eslint.config.js",
 879        "eslint.config.mjs",
 880        "eslint.config.cjs",
 881        "eslint.config.ts",
 882        "eslint.config.cts",
 883        "eslint.config.mts",
 884    ];
 885
 886    pub fn new(node: NodeRuntime) -> Self {
 887        EsLintLspAdapter { node }
 888    }
 889
 890    fn build_destination_path(container_dir: &Path) -> PathBuf {
 891        container_dir.join(format!("vscode-eslint-{}", Self::CURRENT_VERSION))
 892    }
 893}
 894
 895#[async_trait(?Send)]
 896impl LspAdapter for EsLintLspAdapter {
 897    fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
 898        Some(vec![
 899            CodeActionKind::QUICKFIX,
 900            CodeActionKind::new("source.fixAll.eslint"),
 901        ])
 902    }
 903
 904    async fn workspace_configuration(
 905        self: Arc<Self>,
 906        _: &dyn Fs,
 907        delegate: &Arc<dyn LspAdapterDelegate>,
 908        _: Arc<dyn LanguageToolchainStore>,
 909        cx: &mut AsyncApp,
 910    ) -> Result<Value> {
 911        let workspace_root = delegate.worktree_root_path();
 912        let use_flat_config = Self::FLAT_CONFIG_FILE_NAMES
 913            .iter()
 914            .any(|file| workspace_root.join(file).is_file());
 915
 916        let mut default_workspace_configuration = json!({
 917            "validate": "on",
 918            "rulesCustomizations": [],
 919            "run": "onType",
 920            "nodePath": null,
 921            "workingDirectory": {
 922                "mode": "auto"
 923            },
 924            "workspaceFolder": {
 925                "uri": workspace_root,
 926                "name": workspace_root.file_name()
 927                    .unwrap_or(workspace_root.as_os_str())
 928                    .to_string_lossy(),
 929            },
 930            "problems": {},
 931            "codeActionOnSave": {
 932                // We enable this, but without also configuring code_actions_on_format
 933                // in the Zed configuration, it doesn't have an effect.
 934                "enable": true,
 935            },
 936            "codeAction": {
 937                "disableRuleComment": {
 938                    "enable": true,
 939                    "location": "separateLine",
 940                },
 941                "showDocumentation": {
 942                    "enable": true
 943                }
 944            },
 945            "useFlatConfig": use_flat_config,
 946        });
 947
 948        let override_options = cx.update(|cx| {
 949            language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
 950                .and_then(|s| s.settings.clone())
 951        })?;
 952
 953        if let Some(override_options) = override_options {
 954            merge_json_value_into(override_options, &mut default_workspace_configuration);
 955        }
 956
 957        Ok(json!({
 958            "": default_workspace_configuration
 959        }))
 960    }
 961
 962    fn name(&self) -> LanguageServerName {
 963        Self::SERVER_NAME.clone()
 964    }
 965
 966    async fn fetch_latest_server_version(
 967        &self,
 968        _delegate: &dyn LspAdapterDelegate,
 969    ) -> Result<Box<dyn 'static + Send + Any>> {
 970        let url = build_asset_url(
 971            "zed-industries/vscode-eslint",
 972            Self::CURRENT_VERSION_TAG_NAME,
 973            Self::GITHUB_ASSET_KIND,
 974        )?;
 975
 976        Ok(Box::new(GitHubLspBinaryVersion {
 977            name: Self::CURRENT_VERSION.into(),
 978            url,
 979        }))
 980    }
 981
 982    async fn fetch_server_binary(
 983        &self,
 984        version: Box<dyn 'static + Send + Any>,
 985        container_dir: PathBuf,
 986        delegate: &dyn LspAdapterDelegate,
 987    ) -> Result<LanguageServerBinary> {
 988        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 989        let destination_path = Self::build_destination_path(&container_dir);
 990        let server_path = destination_path.join(Self::SERVER_PATH);
 991
 992        if fs::metadata(&server_path).await.is_err() {
 993            remove_matching(&container_dir, |entry| entry != destination_path).await;
 994
 995            let mut response = delegate
 996                .http_client()
 997                .get(&version.url, Default::default(), true)
 998                .await
 999                .context("downloading release")?;
1000            match Self::GITHUB_ASSET_KIND {
1001                AssetKind::TarGz => {
1002                    let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
1003                    let archive = Archive::new(decompressed_bytes);
1004                    archive.unpack(&destination_path).await.with_context(|| {
1005                        format!("extracting {} to {:?}", version.url, destination_path)
1006                    })?;
1007                }
1008                AssetKind::Gz => {
1009                    let mut decompressed_bytes =
1010                        GzipDecoder::new(BufReader::new(response.body_mut()));
1011                    let mut file =
1012                        fs::File::create(&destination_path).await.with_context(|| {
1013                            format!(
1014                                "creating a file {:?} for a download from {}",
1015                                destination_path, version.url,
1016                            )
1017                        })?;
1018                    futures::io::copy(&mut decompressed_bytes, &mut file)
1019                        .await
1020                        .with_context(|| {
1021                            format!("extracting {} to {:?}", version.url, destination_path)
1022                        })?;
1023                }
1024                AssetKind::Zip => {
1025                    extract_zip(&destination_path, response.body_mut())
1026                        .await
1027                        .with_context(|| {
1028                            format!("unzipping {} to {:?}", version.url, destination_path)
1029                        })?;
1030                }
1031            }
1032
1033            let mut dir = fs::read_dir(&destination_path).await?;
1034            let first = dir.next().await.context("missing first file")??;
1035            let repo_root = destination_path.join("vscode-eslint");
1036            fs::rename(first.path(), &repo_root).await?;
1037
1038            #[cfg(target_os = "windows")]
1039            {
1040                handle_symlink(
1041                    repo_root.join("$shared"),
1042                    repo_root.join("client").join("src").join("shared"),
1043                )
1044                .await?;
1045                handle_symlink(
1046                    repo_root.join("$shared"),
1047                    repo_root.join("server").join("src").join("shared"),
1048                )
1049                .await?;
1050            }
1051
1052            self.node
1053                .run_npm_subcommand(&repo_root, "install", &[])
1054                .await?;
1055
1056            self.node
1057                .run_npm_subcommand(&repo_root, "run-script", &["compile"])
1058                .await?;
1059        }
1060
1061        Ok(LanguageServerBinary {
1062            path: self.node.binary_path().await?,
1063            env: None,
1064            arguments: eslint_server_binary_arguments(&server_path),
1065        })
1066    }
1067
1068    async fn cached_server_binary(
1069        &self,
1070        container_dir: PathBuf,
1071        _: &dyn LspAdapterDelegate,
1072    ) -> Option<LanguageServerBinary> {
1073        let server_path =
1074            Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
1075        Some(LanguageServerBinary {
1076            path: self.node.binary_path().await.ok()?,
1077            env: None,
1078            arguments: eslint_server_binary_arguments(&server_path),
1079        })
1080    }
1081}
1082
1083#[cfg(target_os = "windows")]
1084async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
1085    anyhow::ensure!(
1086        fs::metadata(&src_dir).await.is_ok(),
1087        "Directory {src_dir:?} is not present"
1088    );
1089    if fs::metadata(&dest_dir).await.is_ok() {
1090        fs::remove_file(&dest_dir).await?;
1091    }
1092    fs::create_dir_all(&dest_dir).await?;
1093    let mut entries = fs::read_dir(&src_dir).await?;
1094    while let Some(entry) = entries.try_next().await? {
1095        let entry_path = entry.path();
1096        let entry_name = entry.file_name();
1097        let dest_path = dest_dir.join(&entry_name);
1098        fs::copy(&entry_path, &dest_path).await?;
1099    }
1100    Ok(())
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105    use std::path::Path;
1106
1107    use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
1108    use language::language_settings;
1109    use project::{FakeFs, Project};
1110    use serde_json::json;
1111    use unindent::Unindent;
1112    use util::path;
1113
1114    use crate::typescript::{PackageJsonData, TypeScriptContextProvider};
1115
1116    #[gpui::test]
1117    async fn test_outline(cx: &mut TestAppContext) {
1118        let language = crate::language(
1119            "typescript",
1120            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
1121        );
1122
1123        let text = r#"
1124            function a() {
1125              // local variables are omitted
1126              let a1 = 1;
1127              // all functions are included
1128              async function a2() {}
1129            }
1130            // top-level variables are included
1131            let b: C
1132            function getB() {}
1133            // exported variables are included
1134            export const d = e;
1135        "#
1136        .unindent();
1137
1138        let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
1139        let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
1140        assert_eq!(
1141            outline
1142                .items
1143                .iter()
1144                .map(|item| (item.text.as_str(), item.depth))
1145                .collect::<Vec<_>>(),
1146            &[
1147                ("function a()", 0),
1148                ("async function a2()", 1),
1149                ("let b", 0),
1150                ("function getB()", 0),
1151                ("const d", 0),
1152            ]
1153        );
1154    }
1155
1156    #[gpui::test]
1157    async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1158        cx.update(|cx| {
1159            settings::init(cx);
1160            Project::init_settings(cx);
1161            language_settings::init(cx);
1162        });
1163
1164        let package_json_1 = json!({
1165            "dependencies": {
1166                "mocha": "1.0.0",
1167                "vitest": "1.0.0"
1168            },
1169            "scripts": {
1170                "test": ""
1171            }
1172        })
1173        .to_string();
1174
1175        let package_json_2 = json!({
1176            "devDependencies": {
1177                "vitest": "2.0.0"
1178            },
1179            "scripts": {
1180                "test": ""
1181            }
1182        })
1183        .to_string();
1184
1185        let fs = FakeFs::new(executor);
1186        fs.insert_tree(
1187            path!("/root"),
1188            json!({
1189                "package.json": package_json_1,
1190                "sub": {
1191                    "package.json": package_json_2,
1192                    "file.js": "",
1193                }
1194            }),
1195        )
1196        .await;
1197
1198        let provider = TypeScriptContextProvider::new();
1199        let package_json_data = cx
1200            .update(|cx| {
1201                provider.combined_package_json_data(
1202                    fs.clone(),
1203                    path!("/root").as_ref(),
1204                    "sub/file1.js".as_ref(),
1205                    cx,
1206                )
1207            })
1208            .await
1209            .unwrap();
1210        pretty_assertions::assert_eq!(
1211            package_json_data,
1212            PackageJsonData {
1213                jest_package_path: None,
1214                mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
1215                vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
1216                jasmine_package_path: None,
1217                scripts: [
1218                    (
1219                        Path::new(path!("/root/package.json")).into(),
1220                        "test".to_owned()
1221                    ),
1222                    (
1223                        Path::new(path!("/root/sub/package.json")).into(),
1224                        "test".to_owned()
1225                    )
1226                ]
1227                .into_iter()
1228                .collect(),
1229                package_manager: None,
1230            }
1231        );
1232    }
1233}