termesh-tasks 0.1.2

Internal component of a terminal-native, agent-first IDE.
Documentation
use termesh_core::TaskSpec;
use termesh_filesystem::FileSystemService;
use termesh_workspace::{ProjectKind, WorkspaceRoot};

use crate::{
    java::java_tasks, node::node_tasks, python::python_tasks, CargoOutputDecoder,
    TaskOutputDecoder, TaskService, TextProblemDecoder,
};

const TASKS: &[(&str, &str, &str)] = &[
    ("cargo.check", "Check", "check"),
    ("cargo.build", "Build", "build"),
    ("cargo.test", "Test", "test"),
    ("cargo.clippy", "Clippy", "clippy"),
];

#[derive(Debug, Clone, Copy, Default)]
pub struct AdapterTaskService;

impl AdapterTaskService {
    pub fn cargo_only() -> Self {
        Self
    }
}

impl TaskService for AdapterTaskService {
    fn catalog(&self, root: &WorkspaceRoot, fs: &dyn FileSystemService) -> Vec<TaskSpec> {
        let mut tasks = Vec::new();
        if root.kinds.contains(&ProjectKind::Rust) {
            tasks.extend(TASKS.iter().map(|(id, label, subcommand)| TaskSpec {
                id: (*id).into(),
                label: (*label).into(),
                program: "cargo".into(),
                args: vec![
                    (*subcommand).into(),
                    "--message-format=json-diagnostic-rendered-ansi".into(),
                ],
                cwd: root.path.clone(),
            }));
        }
        if root.kinds.contains(&ProjectKind::Node) {
            tasks.extend(node_tasks(fs, &root.path));
        }
        if root.kinds.contains(&ProjectKind::Python) {
            tasks.extend(python_tasks(&root.path));
        }
        if root.kinds.contains(&ProjectKind::Java) {
            tasks.extend(java_tasks(fs, &root.path));
        }
        tasks
    }

    fn decoder(&self, task: &TaskSpec) -> Option<Box<dyn TaskOutputDecoder>> {
        if TASKS.iter().any(|(id, _, _)| *id == task.id) {
            Some(Box::new(CargoOutputDecoder::new(task.cwd.clone())))
        } else {
            Some(Box::new(TextProblemDecoder::new(task.cwd.clone())))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;
    use termesh_filesystem::RealFileSystem;

    fn root(kind: ProjectKind) -> WorkspaceRoot {
        WorkspaceRoot { path: "/p".into(), kind, kinds: vec![kind], detected: true }
    }

    #[test]
    fn the_cargo_adapter_ignores_the_filesystem_it_is_handed() {
        let service = AdapterTaskService::cargo_only();
        let tasks = service.catalog(&root(ProjectKind::Rust), &RealFileSystem);
        assert_eq!(tasks.len(), 4);
    }

    #[test]
    fn rust_projects_get_exactly_four_curated_tasks() {
        let service = AdapterTaskService::cargo_only();
        let tasks = service.catalog(&root(ProjectKind::Rust), &RealFileSystem);
        assert_eq!(
            tasks.iter().map(|task| task.id.as_str()).collect::<Vec<_>>(),
            ["cargo.check", "cargo.build", "cargo.test", "cargo.clippy"]
        );
        assert!(tasks.iter().all(|task| {
            task.program == "cargo"
                && task.args.last().map(String::as_str)
                    == Some("--message-format=json-diagnostic-rendered-ansi")
                && task.cwd == Path::new("/p")
        }));
    }

    #[test]
    fn non_rust_projects_get_no_cargo_tasks() {
        let service = AdapterTaskService::cargo_only();
        assert!(service.catalog(&root(ProjectKind::Node), &RealFileSystem).is_empty());
    }

    #[test]
    fn a_python_project_offers_a_conventional_test_task() {
        let service = AdapterTaskService::cargo_only();
        let tasks = service.catalog(&root(ProjectKind::Python), &RealFileSystem);
        assert!(tasks.iter().any(|task| task.program == "pytest"));
    }
}