OXYGENHUB
◆ main oxygenhub-vscode / src / diff.ts
3.4 KiB · last changed Write the forge's name as OxygenHub in everything people read 1 month ago source blame history raw
1
// Proposals in VS Code's own diff editors (spec §6.1): each side of a
2
// changed file is a read-only document served from the hub, the target's
3
// state on the left and the projected state on the right, so a proposal
4
// can be read and reviewed without checking it out.
5
6
import * as vscode from "vscode";
7
import type * as api from "./hubapi";
8
import { HubAuth } from "./auth";
9
import { DiffRef, decodeDiffQuery, encodeDiffQuery } from "./hub";
10
import type { RepoRef } from "./proposals";
11
12
export const SCHEME = "oxygenhub-proposal";
13
14
/** The document URI of one side of a file in a proposal. */
15
export function diffUri(repo: RepoRef, number: number, side: "base" | "head", path: string): vscode.Uri {
16
  return vscode.Uri.from({ scheme: SCHEME, path: `/${path}`, query: encodeDiffQuery({ base: repo.base, owner: repo.owner, name: repo.name, number, side, path }) });
17
}
18
19
/** The proposal, side and path a document of this scheme shows. */
20
export function refOf(uri: vscode.Uri): DiffRef | undefined {
21
  return uri.scheme === SCHEME ? decodeDiffQuery(uri.query, uri.path) : undefined;
22
}
23
24
/** Serves both sides from the hub; a side where the file is absent is empty. */
25
export class ProposalContentProvider implements vscode.TextDocumentContentProvider, vscode.Disposable {
26
  private readonly emitter = new vscode.EventEmitter<vscode.Uri>();
27
  readonly onDidChange = this.emitter.event;
28
  private readonly open = new Set<string>();
29
30
  constructor(private readonly auth: HubAuth) {}
31
32
  async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
33
    const r = refOf(uri);
34
    if (!r) {
35
      return "";
36
    }
37
    this.open.add(uri.toString());
38
    try {
39
      return (await this.auth.clientFor(r.base).proposalContent(r.owner, r.name, r.number, r.side, r.path)) ?? "";
40
    } catch (e) {
41
      void vscode.window.showWarningMessage(`OxygenHub: could not load ${r.path}: ${(e as Error).message}`);
42
      return "";
43
    }
44
  }
45
46
  /** Reload every open side of a proposal, after it gains changes. */
47
  refresh(base: string, owner: string, name: string, number: number): void {
48
    for (const s of this.open) {
49
      const r = refOf(vscode.Uri.parse(s));
50
      if (r && r.base === base && r.owner === owner && r.name === name && r.number === number) {
51
        this.emitter.fire(vscode.Uri.parse(s));
52
      }
53
    }
54
  }
55
56
  dispose(): void {
57
    this.emitter.dispose();
58
  }
59
}
60
61
function sides(repo: RepoRef, number: number, f: api.ProposalFile): { left: vscode.Uri; right: vscode.Uri } {
62
  return { left: diffUri(repo, number, "base", f.from ?? f.path), right: diffUri(repo, number, "head", f.path) };
63
}
64
65
/** One file in a diff editor. */
66
export async function openFileDiff(repo: RepoRef, number: number, f: api.ProposalFile): Promise<void> {
67
  const { left, right } = sides(repo, number, f);
68
  const title = `#${number} ${f.from ? `${f.from} → ${f.path}` : f.path} (${f.kind})`;
69
  await vscode.commands.executeCommand("vscode.diff", left, right, title, { preview: true });
70
}
71
72
/** Every file of a proposal in one multi-file diff editor. */
73
export async function openAllChanges(repo: RepoRef, p: api.Proposal, files: api.ProposalFile[]): Promise<void> {
74
  if (!files.length) {
75
    void vscode.window.showInformationMessage(`OxygenHub: #${p.number} changes no files.`);
76
    return;
77
  }
78
  const resources = files.map((f) => {
79
    const { left, right } = sides(repo, p.number, f);
80
    return [right, left, right];
81
  });
82
  await vscode.commands.executeCommand("vscode.changes", `#${p.number} ${p.title}`, resources);
83
}