// Proposals in VS Code's own diff editors (spec §6.1): each side of a // changed file is a read-only document served from the hub, the target's // state on the left and the projected state on the right, so a proposal // can be read and reviewed without checking it out. import * as vscode from "vscode"; import type * as api from "./hubapi"; import { HubAuth } from "./auth"; import { DiffRef, decodeDiffQuery, encodeDiffQuery } from "./hub"; import type { RepoRef } from "./proposals"; export const SCHEME = "oxygenhub-proposal"; /** The document URI of one side of a file in a proposal. */ export function diffUri(repo: RepoRef, number: number, side: "base" | "head", path: string): vscode.Uri { return vscode.Uri.from({ scheme: SCHEME, path: `/${path}`, query: encodeDiffQuery({ base: repo.base, owner: repo.owner, name: repo.name, number, side, path }) }); } /** The proposal, side and path a document of this scheme shows. */ export function refOf(uri: vscode.Uri): DiffRef | undefined { return uri.scheme === SCHEME ? decodeDiffQuery(uri.query, uri.path) : undefined; } /** Serves both sides from the hub; a side where the file is absent is empty. */ export class ProposalContentProvider implements vscode.TextDocumentContentProvider, vscode.Disposable { private readonly emitter = new vscode.EventEmitter(); readonly onDidChange = this.emitter.event; private readonly open = new Set(); constructor(private readonly auth: HubAuth) {} async provideTextDocumentContent(uri: vscode.Uri): Promise { const r = refOf(uri); if (!r) { return ""; } this.open.add(uri.toString()); try { return (await this.auth.clientFor(r.base).proposalContent(r.owner, r.name, r.number, r.side, r.path)) ?? ""; } catch (e) { void vscode.window.showWarningMessage(`OxygenHub: could not load ${r.path}: ${(e as Error).message}`); return ""; } } /** Reload every open side of a proposal, after it gains changes. */ refresh(base: string, owner: string, name: string, number: number): void { for (const s of this.open) { const r = refOf(vscode.Uri.parse(s)); if (r && r.base === base && r.owner === owner && r.name === name && r.number === number) { this.emitter.fire(vscode.Uri.parse(s)); } } } dispose(): void { this.emitter.dispose(); } } function sides(repo: RepoRef, number: number, f: api.ProposalFile): { left: vscode.Uri; right: vscode.Uri } { return { left: diffUri(repo, number, "base", f.from ?? f.path), right: diffUri(repo, number, "head", f.path) }; } /** One file in a diff editor. */ export async function openFileDiff(repo: RepoRef, number: number, f: api.ProposalFile): Promise { const { left, right } = sides(repo, number, f); const title = `#${number} ${f.from ? `${f.from} → ${f.path}` : f.path} (${f.kind})`; await vscode.commands.executeCommand("vscode.diff", left, right, title, { preview: true }); } /** Every file of a proposal in one multi-file diff editor. */ export async function openAllChanges(repo: RepoRef, p: api.Proposal, files: api.ProposalFile[]): Promise { if (!files.length) { void vscode.window.showInformationMessage(`OxygenHub: #${p.number} changes no files.`); return; } const resources = files.map((f) => { const { left, right } = sides(repo, p.number, f); return [right, left, right]; }); await vscode.commands.executeCommand("vscode.changes", `#${p.number} ${p.title}`, resources); }