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