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