| 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 |
} |