1137 lines · 39.5 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 futures::future::join_all;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, VariableName};use util::archive::extract_zip;use util::merge_json_value_into;use util::{ResultExt, fs::remove_matching, maybe}; use crate::{PackageJson, PackageJsonData}; #[derive(Debug)]pub(crate) struct TypeScriptContextProvider {    last_package_json: PackageJsonContents,} const TYPESCRIPT_RUNNER_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER")); const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME")); const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME")); const TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_PACKAGE_PATH")); const TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA_PACKAGE_PATH")); const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_PACKAGE_PATH")); const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH")); #[derive(Clone, Debug, Default)]struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>); impl PackageJsonData {    fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {        if self.jest_package_path.is_some() {            task_templates.0.push(TaskTemplate {                label: "jest file test".to_owned(),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "jest".to_owned(),                    "--runInBand".to_owned(),                    VariableName::File.template_value(),                ],                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });            task_templates.0.push(TaskTemplate {                label: format!("jest test {}", VariableName::Symbol.template_value()),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "jest".to_owned(),                    "--runInBand".to_owned(),                    "--testNamePattern".to_owned(),                    format!(                        "\"{}\"",                        TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()                    ),                    VariableName::File.template_value(),                ],                tags: vec![                    "ts-test".to_owned(),                    "js-test".to_owned(),                    "tsx-test".to_owned(),                ],                cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });        }         if self.vitest_package_path.is_some() {            task_templates.0.push(TaskTemplate {                label: format!("{} file test", "vitest".to_owned()),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "vitest".to_owned(),                    "run".to_owned(),                    "--poolOptions.forks.minForks=0".to_owned(),                    "--poolOptions.forks.maxForks=1".to_owned(),                    VariableName::File.template_value(),                ],                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });            task_templates.0.push(TaskTemplate {                label: format!(                    "{} test {}",                    "vitest".to_owned(),                    VariableName::Symbol.template_value(),                ),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "vitest".to_owned(),                    "run".to_owned(),                    "--poolOptions.forks.minForks=0".to_owned(),                    "--poolOptions.forks.maxForks=1".to_owned(),                    "--testNamePattern".to_owned(),                    format!(                        "\"{}\"",                        TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()                    ),                    VariableName::File.template_value(),                ],                tags: vec![                    "ts-test".to_owned(),                    "js-test".to_owned(),                    "tsx-test".to_owned(),                ],                cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });        }         if self.mocha_package_path.is_some() {            task_templates.0.push(TaskTemplate {                label: format!("{} file test", "mocha".to_owned()),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "mocha".to_owned(),                    VariableName::File.template_value(),                ],                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });            task_templates.0.push(TaskTemplate {                label: format!(                    "{} test {}",                    "mocha".to_owned(),                    VariableName::Symbol.template_value(),                ),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "mocha".to_owned(),                    "--grep".to_owned(),                    format!("\"{}\"", VariableName::Symbol.template_value()),                    VariableName::File.template_value(),                ],                tags: vec![                    "ts-test".to_owned(),                    "js-test".to_owned(),                    "tsx-test".to_owned(),                ],                cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });        }         if self.jasmine_package_path.is_some() {            task_templates.0.push(TaskTemplate {                label: format!("{} file test", "jasmine".to_owned()),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "jasmine".to_owned(),                    VariableName::File.template_value(),                ],                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });            task_templates.0.push(TaskTemplate {                label: format!(                    "{} test {}",                    "jasmine".to_owned(),                    VariableName::Symbol.template_value(),                ),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec![                    "exec".to_owned(),                    "--".to_owned(),                    "jasmine".to_owned(),                    format!("--filter={}", VariableName::Symbol.template_value()),                    VariableName::File.template_value(),                ],                tags: vec![                    "ts-test".to_owned(),                    "js-test".to_owned(),                    "tsx-test".to_owned(),                ],                cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });        }         for (path, script) in &self.scripts {            task_templates.0.push(TaskTemplate {                label: format!("package.json > {script}",),                command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),                args: vec!["run".to_owned(), script.to_owned()],                tags: vec!["package-script".into()],                cwd: Some(                    path.parent()                        .unwrap_or(Path::new(""))                        .to_string_lossy()                        .to_string(),                ),                ..TaskTemplate::default()            });        }    }} impl TypeScriptContextProvider {    pub fn new() -> Self {        Self {            last_package_json: PackageJsonContents::default(),        }    }     fn combined_package_json_data(        &self,        fs: Arc<dyn Fs>,        worktree_root: &Path,        file_relative_path: &Path,        cx: &App,    ) -> Task<anyhow::Result<PackageJsonData>> {        let new_json_data = file_relative_path            .ancestors()            .map(|path| worktree_root.join(path))            .map(|parent_path| {                self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)            })            .collect::<Vec<_>>();         cx.background_spawn(async move {            let mut package_json_data = PackageJsonData::default();            for new_data in join_all(new_json_data).await.into_iter().flatten() {                package_json_data.merge(new_data);            }            Ok(package_json_data)        })    }     fn package_json_data(        &self,        directory_path: &Path,        existing_package_json: PackageJsonContents,        fs: Arc<dyn Fs>,        cx: &App,    ) -> Task<anyhow::Result<PackageJsonData>> {        let package_json_path = directory_path.join("package.json");        let metadata_check_fs = fs.clone();        cx.background_spawn(async move {            let metadata = metadata_check_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 = existing_package_json.0.read().await;                contents                    .get(&package_json_path)                    .filter(|package_json| package_json.mtime == mtime)                    .map(|package_json| package_json.data.clone())            };            match existing_data {                Some(existing_data) => Ok(existing_data),                None => {                    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_lenient::Value> =                        serde_json_lenient::from_str(&package_json_string).with_context(|| {                            format!("parsing package.json from {package_json_path:?}")                        })?;                    let new_data =                        PackageJsonData::new(package_json_path.as_path().into(), package_json);                    {                        let mut contents = existing_package_json.0.write().await;                        contents.insert(                            package_json_path,                            PackageJson {                                mtime,                                data: new_data.clone(),                            },                        );                    }                    Ok(new_data)                }            }        })    }} async fn detect_package_manager(    worktree_root: PathBuf,    fs: Arc<dyn Fs>,    package_json_data: Option<PackageJsonData>,) -> &'static str {    if let Some(package_json_data) = package_json_data {        if let Some(package_manager) = package_json_data.package_manager {            return package_manager;        }    }    if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {        return "pnpm";    }    if fs.is_file(&worktree_root.join("yarn.lock")).await {        return "yarn";    }    "npm"} impl ContextProvider for TypeScriptContextProvider {    fn associated_tasks(        &self,        fs: Arc<dyn Fs>,        file: Option<Arc<dyn File>>,        cx: &App,    ) -> Task<Option<TaskTemplates>> {        let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {            return Task::ready(None);        };        let Some(worktree_root) = file.worktree.read(cx).root_dir() else {            return Task::ready(None);        };        let file_relative_path = file.path().clone();        let package_json_data =            self.combined_package_json_data(fs.clone(), &worktree_root, &file_relative_path, cx);         cx.background_spawn(async move {            let mut task_templates = TaskTemplates(Vec::new());            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()            });             match package_json_data.await {                Ok(package_json) => {                    package_json.fill_task_templates(&mut task_templates);                }                Err(e) => {                    log::error!(                        "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"                    );                }            }             Some(task_templates)        })    }     fn build_context(        &self,        current_vars: &task::TaskVariables,        location: ContextLocation<'_>,        _project_env: Option<HashMap<String, String>>,        _toolchains: Arc<dyn LanguageToolchainStore>,        cx: &mut App,    ) -> Task<Result<task::TaskVariables>> {        let mut vars = task::TaskVariables::default();         if let Some(symbol) = current_vars.get(&VariableName::Symbol) {            vars.insert(                TYPESCRIPT_JEST_TEST_NAME_VARIABLE,                replace_test_name_parameters(symbol),            );            vars.insert(                TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,                replace_test_name_parameters(symbol),            );        }        let file_path = location            .file_location            .buffer            .read(cx)            .file()            .map(|file| file.path());         let args = location.worktree_root.zip(location.fs).zip(file_path).map(            |((worktree_root, fs), file_path)| {                (                    self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),                    worktree_root,                    fs,                )            },        );        cx.background_spawn(async move {            if let Some((task, worktree_root, fs)) = args {                let package_json_data = task.await.log_err();                vars.insert(                    TYPESCRIPT_RUNNER_VARIABLE,                    detect_package_manager(worktree_root, fs, package_json_data.clone())                        .await                        .to_owned(),                );                 if let Some(package_json_data) = package_json_data {                    if let Some(path) = package_json_data.jest_package_path {                        vars.insert(                            TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,                            path.parent()                                .unwrap_or(Path::new(""))                                .to_string_lossy()                                .to_string(),                        );                    }                     if let Some(path) = package_json_data.mocha_package_path {                        vars.insert(                            TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,                            path.parent()                                .unwrap_or(Path::new(""))                                .to_string_lossy()                                .to_string(),                        );                    }                     if let Some(path) = package_json_data.vitest_package_path {                        vars.insert(                            TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,                            path.parent()                                .unwrap_or(Path::new(""))                                .to_string_lossy()                                .to_string(),                        );                    }                     if let Some(path) = package_json_data.jasmine_package_path {                        vars.insert(                            TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,                            path.parent()                                .unwrap_or(Path::new(""))                                .to_string_lossy()                                .to_string(),                        );                    }                }            }            Ok(vars)        })    }} 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(),    ]} fn replace_test_name_parameters(test_name: &str) -> String {    let pattern = regex::Regex::new(r"(%|\$)[0-9a-zA-Z]+").unwrap();     pattern.replace_all(test_name, "(.+?)").to_string()

Lines 1–500 of 1137.