1686 lines · 58.1 KB · Rust
RawDownload
use anyhow::{Context as _, Result};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 http_client::github_download::download_server_binary;use itertools::Itertools as _;use language::{    ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,    LspAdapterDelegate, LspInstaller, Toolchain,};use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};use node_runtime::{NodeRuntime, VersionStrategy};use project::{Fs, lsp_store::language_server_settings};use serde_json::{Value, json};use smol::{fs, lock::RwLock, stream::StreamExt};use std::{    borrow::Cow,    ffi::OsString,    path::{Path, PathBuf},    sync::{Arc, LazyLock},};use task::{TaskTemplate, TaskTemplates, VariableName};use util::{ResultExt, fs::remove_matching, maybe};use util::{merge_json_value_into, rel_path::RelPath}; use crate::{PackageJson, PackageJsonData}; pub(crate) struct TypeScriptContextProvider {    fs: Arc<dyn Fs>,    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")); const TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUN_PACKAGE_PATH")); const TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_NODE_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()            });        }         if self.bun_package_path.is_some() {            task_templates.0.push(TaskTemplate {                label: format!("{} file test", "bun test".to_owned()),                command: "bun".to_owned(),                args: vec!["test".to_owned(), VariableName::File.template_value()],                cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });            task_templates.0.push(TaskTemplate {                label: format!("bun test {}", VariableName::Symbol.template_value(),),                command: "bun".to_owned(),                args: vec![                    "test".to_owned(),                    "--test-name-pattern".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_BUN_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });        }         if self.node_package_path.is_some() {            task_templates.0.push(TaskTemplate {                label: format!("{} file test", "node test".to_owned()),                command: "node".to_owned(),                args: vec!["--test".to_owned(), VariableName::File.template_value()],                tags: vec![                    "ts-test".to_owned(),                    "js-test".to_owned(),                    "tsx-test".to_owned(),                ],                cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });            task_templates.0.push(TaskTemplate {                label: format!("node test {}", VariableName::Symbol.template_value()),                command: "node".to_owned(),                args: vec![                    "--test".to_owned(),                    "--test-name-pattern".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_NODE_PACKAGE_PATH_VARIABLE.template_value()),                ..TaskTemplate::default()            });        }         let script_name_counts: HashMap<_, usize> =            self.scripts                .iter()                .fold(HashMap::default(), |mut acc, (_, script)| {                    *acc.entry(script).or_default() += 1;                    acc                });        for (path, script) in &self.scripts {            let label = if script_name_counts.get(script).copied().unwrap_or_default() > 1                && let Some(parent) = path.parent().and_then(|parent| parent.file_name())            {                let parent = parent.to_string_lossy();                format!("{parent}/package.json > {script}")            } else {                format!("package.json > {script}")            };            task_templates.0.push(TaskTemplate {                label,                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(fs: Arc<dyn Fs>) -> Self {        Self {            fs,            last_package_json: PackageJsonContents::default(),        }    }     fn combined_package_json_data(        &self,        fs: Arc<dyn Fs>,        worktree_root: &Path,        file_relative_path: &RelPath,        cx: &App,    ) -> Task<anyhow::Result<PackageJsonData>> {        let new_json_data = file_relative_path            .ancestors()            .map(|path| worktree_root.join(path.as_std_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        && 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,        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(            self.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());

Lines 1–500 of 1686.