// The operation log as a tree view (spec ยง5.9): newest first, with // "Undo to here" on any entry, snapshots hidden unless configured. import * as vscode from "vscode"; import { OperationInfo } from "./oxy"; import { Repository } from "./repository"; export class OperationItem extends vscode.TreeItem { constructor( readonly repository: Repository, readonly op: OperationInfo, readonly stepsBack: number, ) { super(op.command, vscode.TreeItemCollapsibleState.None); this.id = `${repository.root}:${op.id}`; this.description = timeAgo(op.when_ms); this.tooltip = `${op.command}\n${new Date(op.when_ms).toLocaleString()}\n${op.id.slice(0, 12)}`; this.iconPath = new vscode.ThemeIcon(op.current ? "circle-filled" : op.snapshot ? "circle-small" : "circle-outline"); this.contextValue = op.current ? "current" : "operation"; } } export class OperationsProvider implements vscode.TreeDataProvider { private readonly emitter = new vscode.EventEmitter(); readonly onDidChangeTreeData = this.emitter.event; constructor(private readonly repositories: () => Repository[]) {} refresh(): void { this.emitter.fire(); } getTreeItem(element: OperationItem | RepositoryItem): vscode.TreeItem { return element; } async getChildren(element?: OperationItem | RepositoryItem): Promise<(OperationItem | RepositoryItem)[]> { const repos = this.repositories(); if (!element) { if (repos.length === 1) { return this.operations(repos[0]); } return repos.map((r) => new RepositoryItem(r)); } if (element instanceof RepositoryItem) { return this.operations(element.repository); } return []; } private async operations(repo: Repository): Promise { const showSnapshots = vscode.workspace.getConfiguration("oxygen").get("showSnapshotOperations", false); try { const ops = await repo.oxy.opLog(repo.root); const items: OperationItem[] = []; // Steps back count every operation, snapshots included, since undo // skips snapshots itself only when they are the newest entries. let steps = 0; for (const op of ops) { if (showSnapshots || !op.snapshot || op.current) { items.push(new OperationItem(repo, op, steps)); } if (!op.snapshot) { steps += 1; } } return items.slice(0, 200); } catch { return []; } } } export class RepositoryItem extends vscode.TreeItem { constructor(readonly repository: Repository) { super(repository.root.split("/").pop() ?? repository.root, vscode.TreeItemCollapsibleState.Expanded); this.iconPath = new vscode.ThemeIcon("repo"); this.contextValue = "repository"; } } export function timeAgo(ms: number): string { const s = Math.max(0, Math.round((Date.now() - ms) / 1000)); if (s < 45) return "just now"; if (s < 90) return "a minute ago"; if (s < 2700) return `${Math.round(s / 60)} minutes ago`; if (s < 5400) return "an hour ago"; if (s < 79200) return `${Math.round(s / 3600)} hours ago`; if (s < 129600) return "yesterday"; if (s < 2592000) return `${Math.round(s / 86400)} days ago`; return new Date(ms).toLocaleDateString(); }