OXYGENHUB
# 486c15b00314 oxygenhub-vscode / src / review.ts
3.9 KiB · last changed 0.1.1: review comments anchored to atoms through the API; regenerated types 1 month ago source blame history raw
1
// Review threads on the checked-out proposal (spec §6): a comment
2
// controller whose threads are the hub's anchored comments, placed by
3
// asking oxy for each line's atom.
4
5
import * as vscode from "vscode";
6
import type * as api from "./hubapi";
7
import { HubAuth } from "./auth";
8
import type { HubRepo } from "./proposals";
9
10
interface BlameLine {
11
  line: number;
12
  hash: string;
13
  index: number;
14
}
15
16
/** The hub's comments as threads on files under the repository root. */
17
export class ReviewController implements vscode.Disposable {
18
  private readonly controller = vscode.comments.createCommentController("oxygenhub", "Oxygenhub review");
19
  private threads: vscode.CommentThread[] = [];
20
  private active: { repo: HubRepo; number: number } | undefined;
21
22
  constructor(private readonly auth: HubAuth) {
23
    this.controller.commentingRangeProvider = {
24
      provideCommentingRanges: (document) => (this.active && document.uri.fsPath.startsWith(this.active.repo.handle.root) ? [new vscode.Range(0, 0, Math.max(0, document.lineCount - 1), 0)] : []),
25
    };
26
  }
27
28
  /** Show the threads of a proposal; call after checking it out. */
29
  async load(repo: HubRepo, number: number): Promise<void> {
30
    this.clear();
31
    this.active = { repo, number };
32
    const comments = await this.auth.clientFor(repo.base).proposalComments(repo.owner, repo.name, number);
33
    const byPath = new Map<string, api.Comment[]>();
34
    for (const c of comments) {
35
      if (c.path && c.anchor) {
36
        byPath.set(c.path, [...(byPath.get(c.path) ?? []), c]);
37
      }
38
    }
39
    for (const [path, list] of byPath) {
40
      let blame: BlameLine[] = [];
41
      try {
42
        blame = await repo.handle.json<BlameLine[]>(["blame", path, "--json"]);
43
      } catch {
44
        continue;
45
      }
46
      const uri = vscode.Uri.file(`${repo.handle.root}/${path}`);
47
      // One thread per atom.
48
      const byAtom = new Map<string, api.Comment[]>();
49
      for (const c of list) {
50
        const key = `${c.anchor!.change}:${c.anchor!.index}`;
51
        byAtom.set(key, [...(byAtom.get(key) ?? []), c]);
52
      }
53
      for (const [key, cs] of byAtom) {
54
        const [hash, index] = key.split(":");
55
        const line = blame.find((b) => b.hash === hash && String(b.index) === index)?.line;
56
        if (!line) {
57
          continue;
58
        }
59
        const thread = this.controller.createCommentThread(uri, new vscode.Range(line - 1, 0, line - 1, 0), cs.map(toComment));
60
        thread.label = cs[0].resolved ? "resolved" : "review";
61
        thread.state = cs[0].resolved ? vscode.CommentThreadState.Resolved : vscode.CommentThreadState.Unresolved;
62
        thread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded;
63
        this.threads.push(thread);
64
      }
65
    }
66
  }
67
68
  /** Post a reply to a thread, anchored to the line it sits on. */
69
  async reply(reply: vscode.CommentReply): Promise<void> {
70
    const a = this.active;
71
    if (!a) {
72
      return;
73
    }
74
    const rel = reply.thread.uri.fsPath.slice(a.repo.handle.root.length + 1);
75
    const line = reply.thread.range?.start.line ?? 0;
76
    // The hub resolves the line to its atom, so the thread follows the line through rebases.
77
    await postLineComment(this.auth, a.repo, a.number, rel, line + 1, reply.text);
78
    await this.load(a.repo, a.number);
79
  }
80
81
  clear(): void {
82
    this.threads.forEach((t) => t.dispose());
83
    this.threads = [];
84
    this.active = undefined;
85
  }
86
87
  dispose(): void {
88
    this.clear();
89
    this.controller.dispose();
90
  }
91
}
92
93
function toComment(c: api.Comment): vscode.Comment {
94
  return { body: new vscode.MarkdownString(c.body), mode: vscode.CommentMode.Preview, author: { name: c.author }, label: c.resolved ? "resolved" : undefined, timestamp: new Date(c.created_ms) };
95
}
96
97
/** A comment anchored to the atom at `path:line` of the projected state. */
98
async function postLineComment(auth: HubAuth, repo: HubRepo, number: number, path: string, line: number, body: string): Promise<void> {
99
  await auth.clientFor(repo.base).commentProposal(repo.owner, repo.name, number, body, path, line);
100
}