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