| 1 |
// The operation log as a tree view (spec §5.9): newest first, with |
| 2 |
// "Undo to here" on any entry, snapshots hidden unless configured. |
| 3 |
|
| 4 |
import * as vscode from "vscode"; |
| 5 |
import { OperationInfo } from "./oxy"; |
| 6 |
import { Repository } from "./repository"; |
| 7 |
|
| 8 |
export class OperationItem extends vscode.TreeItem { |
| 9 |
constructor( |
| 10 |
readonly repository: Repository, |
| 11 |
readonly op: OperationInfo, |
| 12 |
readonly stepsBack: number, |
| 13 |
) { |
| 14 |
super(op.command, vscode.TreeItemCollapsibleState.None); |
| 15 |
this.id = `${repository.root}:${op.id}`; |
| 16 |
this.description = timeAgo(op.when_ms); |
| 17 |
this.tooltip = `${op.command}\n${new Date(op.when_ms).toLocaleString()}\n${op.id.slice(0, 12)}`; |
| 18 |
this.iconPath = new vscode.ThemeIcon(op.current ? "circle-filled" : op.snapshot ? "circle-small" : "circle-outline"); |
| 19 |
this.contextValue = op.current ? "current" : "operation"; |
| 20 |
} |
| 21 |
} |
| 22 |
|
| 23 |
export class OperationsProvider implements vscode.TreeDataProvider<OperationItem | RepositoryItem> { |
| 24 |
private readonly emitter = new vscode.EventEmitter<void>(); |
| 25 |
readonly onDidChangeTreeData = this.emitter.event; |
| 26 |
|
| 27 |
constructor(private readonly repositories: () => Repository[]) {} |
| 28 |
|
| 29 |
refresh(): void { |
| 30 |
this.emitter.fire(); |
| 31 |
} |
| 32 |
|
| 33 |
getTreeItem(element: OperationItem | RepositoryItem): vscode.TreeItem { |
| 34 |
return element; |
| 35 |
} |
| 36 |
|
| 37 |
async getChildren(element?: OperationItem | RepositoryItem): Promise<(OperationItem | RepositoryItem)[]> { |
| 38 |
const repos = this.repositories(); |
| 39 |
if (!element) { |
| 40 |
if (repos.length === 1) { |
| 41 |
return this.operations(repos[0]); |
| 42 |
} |
| 43 |
return repos.map((r) => new RepositoryItem(r)); |
| 44 |
} |
| 45 |
if (element instanceof RepositoryItem) { |
| 46 |
return this.operations(element.repository); |
| 47 |
} |
| 48 |
return []; |
| 49 |
} |
| 50 |
|
| 51 |
private async operations(repo: Repository): Promise<OperationItem[]> { |
| 52 |
const showSnapshots = vscode.workspace.getConfiguration("oxygen").get<boolean>("showSnapshotOperations", false); |
| 53 |
try { |
| 54 |
const ops = await repo.oxy.opLog(repo.root); |
| 55 |
const items: OperationItem[] = []; |
| 56 |
// Steps back count every operation, snapshots included, since undo |
| 57 |
// skips snapshots itself only when they are the newest entries. |
| 58 |
let steps = 0; |
| 59 |
for (const op of ops) { |
| 60 |
if (showSnapshots || !op.snapshot || op.current) { |
| 61 |
items.push(new OperationItem(repo, op, steps)); |
| 62 |
} |
| 63 |
if (!op.snapshot) { |
| 64 |
steps += 1; |
| 65 |
} |
| 66 |
} |
| 67 |
return items.slice(0, 200); |
| 68 |
} catch { |
| 69 |
return []; |
| 70 |
} |
| 71 |
} |
| 72 |
} |
| 73 |
|
| 74 |
export class RepositoryItem extends vscode.TreeItem { |
| 75 |
constructor(readonly repository: Repository) { |
| 76 |
super(repository.root.split("/").pop() ?? repository.root, vscode.TreeItemCollapsibleState.Expanded); |
| 77 |
this.iconPath = new vscode.ThemeIcon("repo"); |
| 78 |
this.contextValue = "repository"; |
| 79 |
} |
| 80 |
} |
| 81 |
|
| 82 |
export function timeAgo(ms: number): string { |
| 83 |
const s = Math.max(0, Math.round((Date.now() - ms) / 1000)); |
| 84 |
if (s < 45) return "just now"; |
| 85 |
if (s < 90) return "a minute ago"; |
| 86 |
if (s < 2700) return `${Math.round(s / 60)} minutes ago`; |
| 87 |
if (s < 5400) return "an hour ago"; |
| 88 |
if (s < 79200) return `${Math.round(s / 3600)} hours ago`; |
| 89 |
if (s < 129600) return "yesterday"; |
| 90 |
if (s < 2592000) return `${Math.round(s / 86400)} days ago`; |
| 91 |
return new Date(ms).toLocaleDateString(); |
| 92 |
} |