975 lines · 33.1 KB · Rust
RawDownload
use anyhow::{Context as _, Result};use async_compression::futures::bufread::GzipDecoder;use async_tar::Archive;use async_trait::async_trait;use chrono::{DateTime, Local};use collections::HashMap;use gpui::{App, AppContext, AsyncApp, Task};use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};use language::{    ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,};use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};use node_runtime::NodeRuntime;use project::{Fs, lsp_store::language_server_settings};use serde_json::{Value, json};use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};use std::{    any::Any,    borrow::Cow,    ffi::OsString,    path::{Path, PathBuf},    sync::Arc,};use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};use util::archive::extract_zip;use util::merge_json_value_into;use util::{ResultExt, fs::remove_matching, maybe}; pub(crate) struct TypeScriptContextProvider {    last_package_json: PackageJsonContents,} const TYPESCRIPT_RUNNER_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA")); const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUILD_SCRIPT"));const TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_TEST_SCRIPT")); #[derive(Clone, Default)]struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>); struct PackageJson {    mtime: DateTime<Local>,    data: PackageJsonData,} #[derive(Clone, Copy, Default)]struct PackageJsonData {    jest: bool,    mocha: bool,    vitest: bool,    jasmine: bool,    build_script: bool,    test_script: bool,    runner: Runner,} #[derive(Clone, Copy, Default)]enum Runner {    #[default]    Npm,    Npx,    Pnpm,} impl PackageJsonData {    fn new(package_json: HashMap<String, Value>) -> Self {        let mut build_script = false;        let mut test_script = false;        if let Some(serde_json::Value::Object(scripts)) = package_json.get("scripts") {            build_script |= scripts.contains_key("build");            test_script |= scripts.contains_key("test");        }         let mut jest = false;        let mut mocha = false;        let mut vitest = false;        let mut jasmine = false;        if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {            jest |= dependencies.contains_key("jest");            mocha |= dependencies.contains_key("mocha");            vitest |= dependencies.contains_key("vitest");            jasmine |= dependencies.contains_key("jasmine");        }        if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")        {            jest |= dev_dependencies.contains_key("jest");            mocha |= dev_dependencies.contains_key("mocha");            vitest |= dev_dependencies.contains_key("vitest");            jasmine |= dev_dependencies.contains_key("jasmine");        }         let mut runner = Runner::Npm;        if which::which("pnpm").is_ok() {            runner = Runner::Pnpm;        } else if which::which("npx").is_ok() {            runner = Runner::Npx;        }         Self {            jest,            mocha,            vitest,            jasmine,            build_script,            test_script,            runner,        }    }     fn fill_variables(&self, variables: &mut TaskVariables) {        let runner = match self.runner {            Runner::Npm => "npm",            Runner::Npx => "npx",            Runner::Pnpm => "pnpm",        };        variables.insert(TYPESCRIPT_RUNNER_VARIABLE, runner.to_owned());         if self.jest {            variables.insert(TYPESCRIPT_JEST_TASK_VARIABLE, "jest".to_owned());        }        if self.mocha {            variables.insert(TYPESCRIPT_MOCHA_TASK_VARIABLE, "mocha".to_owned());        }        if self.vitest {            variables.insert(TYPESCRIPT_VITEST_TASK_VARIABLE, "vitest".to_owned());        }        if self.jasmine {            variables.insert(TYPESCRIPT_JASMINE_TASK_VARIABLE, "jasmine".to_owned());        }        if self.build_script {            variables.insert(TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE, "build".to_owned());        }        if self.test_script {            variables.insert(TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE, "test".to_owned());        }    }} impl TypeScriptContextProvider {    pub fn new() -> Self {        TypeScriptContextProvider {            last_package_json: PackageJsonContents::default(),        }    }} impl ContextProvider for TypeScriptContextProvider {    fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Option<TaskTemplates> {        let mut task_templates = TaskTemplates(Vec::new());         // Jest tasks        task_templates.0.push(TaskTemplate {            label: format!(                "{} file test",                TYPESCRIPT_JEST_TASK_VARIABLE.template_value()            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),                VariableName::RelativeFile.template_value(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });        task_templates.0.push(TaskTemplate {            label: format!(                "{} test {}",                TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),                VariableName::Symbol.template_value(),            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),                "--testNamePattern".to_owned(),                format!("\"{}\"", VariableName::Symbol.template_value()),                VariableName::RelativeFile.template_value(),            ],            tags: vec![                "ts-test".to_owned(),                "js-test".to_owned(),                "tsx-test".to_owned(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });         // Vitest tasks        task_templates.0.push(TaskTemplate {            label: format!(                "{} file test",                TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),                "run".to_owned(),                VariableName::RelativeFile.template_value(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });        task_templates.0.push(TaskTemplate {            label: format!(                "{} test {}",                TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),                VariableName::Symbol.template_value(),            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),                "run".to_owned(),                "--testNamePattern".to_owned(),                format!("\"{}\"", VariableName::Symbol.template_value()),                VariableName::RelativeFile.template_value(),            ],            tags: vec![                "ts-test".to_owned(),                "js-test".to_owned(),                "tsx-test".to_owned(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });         // Mocha tasks        task_templates.0.push(TaskTemplate {            label: format!(                "{} file test",                TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value()            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),                VariableName::RelativeFile.template_value(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });        task_templates.0.push(TaskTemplate {            label: format!(                "{} test {}",                TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),                VariableName::Symbol.template_value(),            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),                "--grep".to_owned(),                format!("\"{}\"", VariableName::Symbol.template_value()),                VariableName::RelativeFile.template_value(),            ],            tags: vec![                "ts-test".to_owned(),                "js-test".to_owned(),                "tsx-test".to_owned(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });         // Jasmine tasks        task_templates.0.push(TaskTemplate {            label: format!(                "{} file test",                TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value()            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),                VariableName::RelativeFile.template_value(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });        task_templates.0.push(TaskTemplate {            label: format!(                "{} test {}",                TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),                VariableName::Symbol.template_value(),            ),            command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),            args: vec![                TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),                format!("--filter={}", VariableName::Symbol.template_value()),                VariableName::RelativeFile.template_value(),            ],            tags: vec![                "ts-test".to_owned(),                "js-test".to_owned(),                "tsx-test".to_owned(),            ],            cwd: Some(VariableName::WorktreeRoot.template_value()),            ..TaskTemplate::default()        });         for package_json_script in [            TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE,            TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE,        ] {            task_templates.0.push(TaskTemplate {                label: format!(                    "package.json script {}",                    package_json_script.template_value()                ),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "--prefix".to_owned(),                    VariableName::WorktreeRoot.template_value(),                    "run".to_owned(),                    package_json_script.template_value(),                ],                tags: vec!["package-script".into()],                cwd: Some(VariableName::WorktreeRoot.template_value()),                ..TaskTemplate::default()            });        }         task_templates.0.push(TaskTemplate {            label: format!(                "execute selection {}",                VariableName::SelectedText.template_value()            ),            command: "node".to_owned(),            args: vec![                "-e".to_owned(),                format!("\"{}\"", VariableName::SelectedText.template_value()),            ],            ..TaskTemplate::default()        });         Some(task_templates)    }     fn build_context(        &self,        _variables: &task::TaskVariables,        location: ContextLocation<'_>,        _project_env: Option<HashMap<String, String>>,        _toolchains: Arc<dyn LanguageToolchainStore>,        cx: &mut App,    ) -> Task<Result<task::TaskVariables>> {        let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {            return Task::ready(Ok(task::TaskVariables::default()));        };         let package_json_contents = self.last_package_json.clone();        cx.background_spawn(async move {            let variables = package_json_variables(fs, worktree_root, package_json_contents)                .await                .context("package.json context retrieval")                .log_err()                .unwrap_or_else(task::TaskVariables::default);            Ok(variables)        })    }} async fn package_json_variables(    fs: Arc<dyn Fs>,    worktree_root: PathBuf,    package_json_contents: PackageJsonContents,) -> anyhow::Result<task::TaskVariables> {    let package_json_path = worktree_root.join("package.json");    let metadata = fs        .metadata(&package_json_path)        .await        .with_context(|| format!("getting metadata for {package_json_path:?}"))?        .with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;    let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());    let existing_data = {        let contents = package_json_contents.0.read().await;        contents            .get(&package_json_path)            .filter(|package_json| package_json.mtime == mtime)            .map(|package_json| package_json.data)    };     let mut variables = TaskVariables::default();    if let Some(existing_data) = existing_data {        existing_data.fill_variables(&mut variables);    } else {        let package_json_string = fs            .load(&package_json_path)            .await            .with_context(|| format!("loading package.json from {package_json_path:?}"))?;        let package_json: HashMap<String, serde_json::Value> =            serde_json::from_str(&package_json_string)                .with_context(|| format!("parsing package.json from {package_json_path:?}"))?;        let new_data = PackageJsonData::new(package_json);        new_data.fill_variables(&mut variables);        {            let mut contents = package_json_contents.0.write().await;            contents.insert(                package_json_path,                PackageJson {                    mtime,                    data: new_data,                },            );        }    }     Ok(variables)} fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {    vec![server_path.into(), "--stdio".into()]} fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {    vec![        "--max-old-space-size=8192".into(),        server_path.into(),        "--stdio".into(),    ]} pub struct TypeScriptLspAdapter {    node: NodeRuntime,} impl TypeScriptLspAdapter {    const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";    const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";    const SERVER_NAME: LanguageServerName =        LanguageServerName::new_static("typescript-language-server");    const PACKAGE_NAME: &str = "typescript";    pub fn new(node: NodeRuntime) -> Self {        TypeScriptLspAdapter { node }    }    async fn tsdk_path(fs: &dyn Fs, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {        let is_yarn = adapter            .read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))            .await            .is_ok();         let tsdk_path = if is_yarn {            ".yarn/sdks/typescript/lib"        } else {            "node_modules/typescript/lib"        };         if fs            .is_dir(&adapter.worktree_root_path().join(tsdk_path))            .await        {            Some(tsdk_path)        } else {            None        }    }} struct TypeScriptVersions {    typescript_version: String,    server_version: String,} #[async_trait(?Send)]impl LspAdapter for TypeScriptLspAdapter {    fn name(&self) -> LanguageServerName {        Self::SERVER_NAME.clone()    }     async fn fetch_latest_server_version(        &self,        _: &dyn LspAdapterDelegate,    ) -> Result<Box<dyn 'static + Send + Any>> {        Ok(Box::new(TypeScriptVersions {            typescript_version: self.node.npm_package_latest_version("typescript").await?,            server_version: self                .node                .npm_package_latest_version("typescript-language-server")                .await?,        }) as Box<_>)    }     async fn check_if_version_installed(        &self,        version: &(dyn 'static + Send + Any),        container_dir: &PathBuf,        _: &dyn LspAdapterDelegate,    ) -> Option<LanguageServerBinary> {        let version = version.downcast_ref::<TypeScriptVersions>().unwrap();        let server_path = container_dir.join(Self::NEW_SERVER_PATH);         let should_install_language_server = self            .node            .should_install_npm_package(

Lines 1–500 of 975.