OXYGENHUB
◆ v0.16.1 oxygen-vscode / src / operations.ts
3.5 KiB · last changed 0.15.0: a splash of colour 1 month ago source blame history raw
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
import { icon, operationLook } from "./style";
8
9
export class OperationItem extends vscode.TreeItem {
10
  constructor(
11
    readonly repository: Repository,
12
    readonly op: OperationInfo,
13
    readonly stepsBack: number,
14
  ) {
15
    const look = operationLook(op.command, op.snapshot);
16
    super(look.label, vscode.TreeItemCollapsibleState.None);
17
    this.id = `${repository.root}:${op.id}`;
18
    this.description = `${op.current ? "● now · " : ""}${look.kind} · ${timeAgo(op.when_ms)}`;
19
    this.tooltip = new vscode.MarkdownString(
20
      `$(${look.icon}) **${look.label}**\n\n\`${op.command.replace(/`/g, "'")}\`\n\n${new Date(op.when_ms).toLocaleString()} · \`${op.id.slice(0, 12)}\``,
21
      true,
22
    );
23
    this.iconPath = icon(op.current ? "pass-filled" : look.icon, op.current ? "oxygen.accent" : look.color);
24
    this.contextValue = op.current ? "current" : "operation";
25
  }
26
}
27
28
export class OperationsProvider implements vscode.TreeDataProvider<OperationItem | RepositoryItem> {
29
  private readonly emitter = new vscode.EventEmitter<void>();
30
  readonly onDidChangeTreeData = this.emitter.event;
31
32
  constructor(private readonly repositories: () => Repository[]) {}
33
34
  refresh(): void {
35
    this.emitter.fire();
36
  }
37
38
  getTreeItem(element: OperationItem | RepositoryItem): vscode.TreeItem {
39
    return element;
40
  }
41
42
  async getChildren(element?: OperationItem | RepositoryItem): Promise<(OperationItem | RepositoryItem)[]> {
43
    const repos = this.repositories();
44
    if (!element) {
45
      if (repos.length === 1) {
46
        return this.operations(repos[0]);
47
      }
48
      return repos.map((r) => new RepositoryItem(r));
49
    }
50
    if (element instanceof RepositoryItem) {
51
      return this.operations(element.repository);
52
    }
53
    return [];
54
  }
55
56
  private async operations(repo: Repository): Promise<OperationItem[]> {
57
    const showSnapshots = vscode.workspace.getConfiguration("oxygen").get<boolean>("showSnapshotOperations", false);
58
    try {
59
      const ops = await repo.oxy.opLog(repo.root);
60
      const items: OperationItem[] = [];
61
      // Steps back count every operation, snapshots included, since undo
62
      // skips snapshots itself only when they are the newest entries.
63
      let steps = 0;
64
      for (const op of ops) {
65
        if (showSnapshots || !op.snapshot || op.current) {
66
          items.push(new OperationItem(repo, op, steps));
67
        }
68
        if (!op.snapshot) {
69
          steps += 1;
70
        }
71
      }
72
      return items.slice(0, 200);
73
    } catch {
74
      return [];
75
    }
76
  }
77
}
78
79
export class RepositoryItem extends vscode.TreeItem {
80
  constructor(readonly repository: Repository) {
81
    super(repository.root.split("/").pop() ?? repository.root, vscode.TreeItemCollapsibleState.Expanded);
82
    this.iconPath = icon("repo", "oxygen.accent");
83
    this.contextValue = "repository";
84
  }
85
}
86
87
export function timeAgo(ms: number): string {
88
  const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
89
  if (s < 45) return "just now";
90
  if (s < 90) return "a minute ago";
91
  if (s < 2700) return `${Math.round(s / 60)} minutes ago`;
92
  if (s < 5400) return "an hour ago";
93
  if (s < 79200) return `${Math.round(s / 3600)} hours ago`;
94
  if (s < 129600) return "yesterday";
95
  if (s < 2592000) return `${Math.round(s / 86400)} days ago`;
96
  return new Date(ms).toLocaleDateString();
97
}