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