import { listLocatedTasks } from "@/commands/listLocatedTasks"; import { resolveTaskLocations } from "@/commands/resolveTaskLocations"; import { Task } from "@/model/task"; import { LocatedTask, TaskLocation } from "@/model/taskLocation"; import { TaskStatus, TASK_STATUS_META, TASK_STATUS_ORDER, } from "@/model/taskStatus"; import { IConfigProvider, ITaskRepository } from "@/ports"; import { TASK_SCOPE_LABELS, TASK_STATUS_LABELS } from "@/ui/taskLabels"; import * as vscode from "vscode"; import { TaskTreeItem } from "./taskTreeItem"; class ScopeTreeItem extends vscode.TreeItem { readonly location: TaskLocation; readonly locatedTasks: LocatedTask[]; constructor(location: TaskLocation, locatedTasks: LocatedTask[]) { super( TASK_SCOPE_LABELS[location.scope], vscode.TreeItemCollapsibleState.Expanded, ); this.location = location; this.locatedTasks = locatedTasks; this.id = `scope-${location.scope}`; this.contextValue = "taskScope"; this.description = `(${locatedTasks.length})`; this.iconPath = location.scope === "project" ? new vscode.ThemeIcon("root-folder") : new vscode.ThemeIcon("globe"); } } class TaskGroupTreeItem extends vscode.TreeItem { readonly location: TaskLocation; readonly tasks: Task[]; constructor(status: TaskStatus, location: TaskLocation, tasks: Task[]) { super( TASK_STATUS_LABELS[status], vscode.TreeItemCollapsibleState.Collapsed, ); this.location = location; this.tasks = tasks; this.description = `(${tasks.length})`; this.id = `group-${location.scope}-${status}`; this.contextValue = "taskGroup"; this.iconPath = new vscode.ThemeIcon( TASK_STATUS_META[status].icon, status === TaskStatus.UNKNOWN ? new vscode.ThemeColor("charts.red") : undefined, ); } } export class TaskTreeProvider implements vscode.TreeDataProvider { #onDidChangeTreeData = new vscode.EventEmitter< vscode.TreeItem | undefined | null | void >(); readonly onDidChangeTreeData = this.#onDidChangeTreeData.event; #taskStore: ITaskRepository; #configStore: IConfigProvider; constructor(taskStore: ITaskRepository, configStore: IConfigProvider) { this.#taskStore = taskStore; this.#configStore = configStore; } refresh(): void { this.#onDidChangeTreeData.fire(); } getTreeItem(element: vscode.TreeItem): vscode.TreeItem { return element; } async getChildren(element?: vscode.TreeItem): Promise { if (!element) { return this.getRootChildren(); } if (element instanceof ScopeTreeItem) { return this.getStatusGroups(element); } if (element instanceof TaskGroupTreeItem) { return element.tasks.map( (task) => new TaskTreeItem(task, element.location), ); } return []; } private async getRootChildren(): Promise { const locations = resolveTaskLocations(this.#configStore); if (locations.length === 0) { return [ new vscode.TreeItem( "Configure project or global task path in settings", ), ]; } const result = await listLocatedTasks(this.#taskStore, locations); if (result.isErr()) { return [new vscode.TreeItem(result.error.message)]; } const byScope = new Map(); for (const location of locations) { byScope.set(location.scope, []); } for (const located of result.value) { byScope.get(located.location.scope)?.push(located); } return locations.map((location) => { const tasks = byScope.get(location.scope) ?? []; return new ScopeTreeItem(location, tasks); }); } private getStatusGroups(scope: ScopeTreeItem): vscode.TreeItem[] { const tasks = scope.locatedTasks.map((item) => item.task); return TASK_STATUS_ORDER.filter((status) => { const groupTasks = tasks.filter((t) => t.status === status); return !(groupTasks.length === 0 && TASK_STATUS_META[status].fallback); }).map((status) => { const groupTasks = tasks.filter((t) => t.status === status); return new TaskGroupTreeItem(status, scope.location, groupTasks); }); } }