f0b06dea Andy McCall 1 month ago
1
// Activation: sign in, find the Oxygen extension's API, register the
2
// remote provider, wire the views, the review controller and the commands.
3
4
import * as vscode from "vscode";
5
import { HubAuth } from "./auth";
6
import { ChecksBar } from "./checks";
bf73998c Andy McCall 1 month ago ↶
7
import { ProposalContentProvider, SCHEME, openAllChanges, openFileDiff, refOf } from "./diff";
263de209 Andy McCall 1 month ago ↶
8
import { FlowLogs, FlowProvider, JobNode, LOG_SCHEME, RunNode, openLog, useLogs } from "./flow";
f0b06dea Andy McCall 1 month ago
9
import { hubUrl } from "./hub";
b2dd082c Andy McCall 1 month ago ↶
10
import { InboxBar } from "./inbox";
1cc0ff44 Andy McCall 1 month ago ↶
11
import { draftRelease, ReleasesProvider } from "./releases";
d21a8595 Andy McCall 23 days ago ↶
12
import { DiffusionPinNode, DiffusionProvider, diffusionTab, pinUrl } from "./diffusion";
f902d5b8 Andy McCall 1 month ago ↶
13
import { ReferenceCompletions, ReferenceHovers, ReferenceIndex, SELECTOR } from "./references";
95f69b4a Andy McCall 1 month ago ↶
14
import { annotateTodo, issueBody, todoTitle } from "./refs";
f0b06dea Andy McCall 1 month ago
15
import { IssueNode, IssuesProvider, showIssue } from "./issues";
16
import type { OxygenAPI } from "./oxygenapi";
bf73998c Andy McCall 1 month ago ↶
17
import { HubRepo, ProposalFileNode, ProposalNode, ProposalsProvider, showDetail } from "./proposals";
f0b06dea Andy McCall 1 month ago
18
import { makeProvider } from "./provider";
19
import { ReviewController } from "./review";
20
21
let auth: HubAuth;
22
let oxygen: OxygenAPI | undefined;
23
let proposals: ProposalsProvider;
24
let issues: IssuesProvider;
25
let review: ReviewController;
26
let bar: ChecksBar;
bf73998c Andy McCall 1 month ago ↶
27
let content: ProposalContentProvider;
b2dd082c Andy McCall 1 month ago ↶
28
let inbox: InboxBar;
f902d5b8 Andy McCall 1 month ago ↶
29
let references: ReferenceIndex;
1cc0ff44 Andy McCall 1 month ago ↶
30
let releases: ReleasesProvider;
d21a8595 Andy McCall 23 days ago ↶
31
let diffusion: DiffusionProvider;
263de209 Andy McCall 1 month ago ↶
32
let flow: FlowProvider;
f0b06dea Andy McCall 1 month ago
33
34
export async function activate(context: vscode.ExtensionContext): Promise<void> {
35
auth = new HubAuth(context.secrets);
36
await auth.load();
37
context.subscriptions.push(auth);
38
85a87a2d Andy McCall 1 month ago ↶
39
// Oxygen VCS may be missing or fail to start (two copies of it installed
40
// at once): OxygenHub still starts, so signing in and the hub views work.
41
const ext = vscode.extensions.getExtension<OxygenAPI>("oxygenhub.oxygen-vscode");
42
try {
43
oxygen = ext ? ((await ext.activate()) as OxygenAPI | undefined) : undefined;
44
} catch {
45
oxygen = undefined;
46
}
f0b06dea Andy McCall 1 month ago
47
if (!oxygen) {
85a87a2d Andy McCall 1 month ago ↶
48
void vscode.window.showWarningMessage("OxygenHub: the Oxygen VCS extension is not available; install Oxygen VCS (oxygenhub.oxygen-vscode) 0.17.1 or later, and only one copy of it.");
f0b06dea Andy McCall 1 month ago
49
} else {
50
context.subscriptions.push(oxygen.registerRemoteProvider(makeProvider({ bases: () => auth.bases() })));
51
context.subscriptions.push(oxygen.onDidChangeRepositories(() => refreshAll()));
52
}
53
54
proposals = new ProposalsProvider(() => oxygen, auth);
55
issues = new IssuesProvider(() => proposals.repos(), auth);
56
review = new ReviewController(auth);
57
bar = new ChecksBar(auth, () => proposals.repos());
b2dd082c Andy McCall 1 month ago ↶
58
inbox = new InboxBar(auth, async (t) => {
59
const repo = (await proposals.repos()).find((r) => r.base === t.base && r.owner === t.owner && r.name === t.name);
60
if (!repo) {
61
return false;
62
}
63
const client = auth.clientFor(repo.base);
64
if (t.kind === "proposal") {
65
const [p, checks] = await Promise.all([client.proposal(repo.owner, repo.name, t.number), client.proposalChecks(repo.owner, repo.name, t.number).catch(() => [])]);
66
showDetail(repo, p, checks);
67
} else {
68
await showIssue(auth, repo, await client.issue(repo.owner, repo.name, t.number));
69
}
70
return true;
71
});
72
context.subscriptions.push(inbox);
1cc0ff44 Andy McCall 1 month ago ↶
73
releases = new ReleasesProvider(() => proposals.repos(), auth);
74
context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygenhub.releases", releases));
d21a8595 Andy McCall 23 days ago ↶
75
diffusion = new DiffusionProvider(() => proposals.repos(), auth);
76
context.subscriptions.push(
77
vscode.window.registerTreeDataProvider("oxygenhub.diffusion", diffusion),
78
// A bump on this repository opens beside the code, as any proposal does;
79
// the rest open on the hub.
80
vscode.commands.registerCommand("oxygenhub.openDiffusionPin", async (node: DiffusionPinNode) => {
81
const bump = node.pin.bump;
82
if (node.side === "takes" && bump !== null && bump !== undefined) {
83
try {
84
const client = auth.clientFor(node.repo.base);
85
const [p, checks] = await Promise.all([client.proposal(node.repo.owner, node.repo.name, bump), client.proposalChecks(node.repo.owner, node.repo.name, bump).catch(() => [])]);
86
showDetail(node.repo, p, checks);
87
return;
88
} catch (e) {
89
void vscode.window.showWarningMessage(`OxygenHub: ${(e as Error).message}`);
90
}
91
}
92
void vscode.env.openExternal(vscode.Uri.parse(pinUrl(node)));
93
}),
94
vscode.commands.registerCommand("oxygenhub.openDiffusionOnHub", (node?: DiffusionPinNode | { kind: "repo"; repo: HubRepo }) => {
95
if (node) {
96
void vscode.env.openExternal(vscode.Uri.parse(node.kind === "pin" ? pinUrl(node) : diffusionTab(node.repo)));
97
}
98
}),
99
);
263de209 Andy McCall 1 month ago ↶
100
flow = new FlowProvider(() => proposals.repos(), auth);
101
const flowLogs = new FlowLogs(auth);
102
useLogs(flowLogs);
103
context.subscriptions.push(
104
flow,
105
flowLogs,
106
vscode.window.registerTreeDataProvider("oxygenhub.flow", flow),
107
vscode.workspace.registerTextDocumentContentProvider(LOG_SCHEME, flowLogs),
108
vscode.commands.registerCommand("oxygenhub.flowRefresh", () => flow.refresh()),
109
vscode.commands.registerCommand("oxygenhub.runPipeline", (node?: { repo?: HubRepo }) => flow.start(node?.repo)),
110
vscode.commands.registerCommand("oxygenhub.rerunFlow", (node: RunNode) => flow.rerun(node)),
111
vscode.commands.registerCommand("oxygenhub.flowLog", (node: JobNode) => openLog(node.repo, node.run, node.job)),
112
vscode.commands.registerCommand("oxygenhub.openFlowRun", (node: RunNode) => vscode.env.openExternal(vscode.Uri.parse(node.run.url))),
113
vscode.commands.registerCommand("oxygenhub.flowRecent", (repo?: HubRepo) => flow.recent(repo)),
114
);
f902d5b8 Andy McCall 1 month ago ↶
115
references = new ReferenceIndex(auth, () => proposals.repos());
116
context.subscriptions.push(
117
vscode.languages.registerCompletionItemProvider(SELECTOR, new ReferenceCompletions(references), "#", "@"),
118
vscode.languages.registerHoverProvider(SELECTOR, new ReferenceHovers(references)),
119
);
bf73998c Andy McCall 1 month ago ↶
120
content = new ProposalContentProvider(auth);
121
context.subscriptions.push(content, vscode.workspace.registerTextDocumentContentProvider(SCHEME, content));
f0b06dea Andy McCall 1 month ago
122
context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygenhub.proposals", proposals), vscode.window.registerTreeDataProvider("oxygenhub.issues", issues), review, bar);
123
context.subscriptions.push(auth.onDidChange(() => refreshAll()));
124
125
context.subscriptions.push(
126
vscode.commands.registerCommand("oxygenhub.signIn", async () => {
127
const repos = oxygen ? await guessHubs() : [];
128
await auth.signIn(repos[0]);
129
refreshAll();
130
}),
131
vscode.commands.registerCommand("oxygenhub.signOut", async () => {
132
const pick = await vscode.window.showQuickPick(auth.all().map((s) => s.base), { placeHolder: "Sign out of" });
133
if (pick) {
134
await auth.removeSession(pick);
135
refreshAll();
136
}
137
}),
138
vscode.commands.registerCommand("oxygenhub.refresh", () => refreshAll()),
b2dd082c Andy McCall 1 month ago ↶
139
vscode.commands.registerCommand("oxygenhub.inbox", () => inbox.show()),
1cc0ff44 Andy McCall 1 month ago ↶
140
vscode.commands.registerCommand("oxygenhub.draftRelease", async (node?: { repo?: HubRepo }) => {
141
const repos = await proposals.repos();
142
const repo = node?.repo ?? (repos.length === 1 ? repos[0] : (await vscode.window.showQuickPick(repos.map((r) => ({ label: `${r.owner}/${r.name}`, r })), { placeHolder: "Repository" }))?.r);
143
if (!repo) {
144
return;
145
}
146
try {
147
const r = await draftRelease(auth, repo);
148
if (r) {
149
releases.refresh();
092b8bf9 Andy McCall 1 month ago ↶
150
const open = await vscode.window.showInformationMessage(`OxygenHub: released ${r.tag}.`, "Open on Hub");
1cc0ff44 Andy McCall 1 month ago ↶
151
if (open) {
152
void vscode.env.openExternal(vscode.Uri.parse(r.url));
153
}
154
}
155
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
156
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
1cc0ff44 Andy McCall 1 month ago ↶
157
}
158
}),
f0b06dea Andy McCall 1 month ago
159
vscode.commands.registerCommand("oxygenhub.openProposal", async (node?: ProposalNode) => {
160
const n = node ?? (await pickProposal());
161
if (!n) {
162
return;
163
}
164
const checks = await auth.clientFor(n.repo.base).proposalChecks(n.repo.owner, n.repo.name, n.p.number).catch(() => []);
165
showDetail(n.repo, n.p, checks);
166
}),
167
vscode.commands.registerCommand("oxygenhub.openOnHub", (node?: ProposalNode | IssueNode) => {
168
if (!node) {
169
return;
170
}
171
const target = node.kind === "proposal" ? { kind: "proposal", number: node.p.number } : { kind: "issue", number: node.i.number };
172
void vscode.env.openExternal(vscode.Uri.parse(hubUrl(node.repo.base, node.repo.owner, node.repo.name, target)));
173
}),
bf73998c Andy McCall 1 month ago ↶
174
vscode.commands.registerCommand("oxygenhub.openProposalFile", async (node: ProposalFileNode) => {
175
await openFileDiff(node.repo, node.p.number, node.f);
176
await review.loadDiff(node.repo, node.p.number).catch(() => undefined);
177
}),
178
vscode.commands.registerCommand("oxygenhub.openProposalChanges", (...args: unknown[]) =>
179
withProposal(args, async (repo, number) => {
180
try {
181
const client = auth.clientFor(repo.base);
182
const [p, files] = await Promise.all([client.proposal(repo.owner, repo.name, number), client.proposalFiles(repo.owner, repo.name, number)]);
183
await openAllChanges(repo, p, files);
184
await review.loadDiff(repo, number);
185
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
186
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
bf73998c Andy McCall 1 month ago ↶
187
}
188
}),
189
),
f0b06dea Andy McCall 1 month ago
190
vscode.commands.registerCommand("oxygenhub.checkoutProposal", (...args: unknown[]) => withProposal(args, checkout)),
191
vscode.commands.registerCommand("oxygenhub.review", (...args: unknown[]) => withProposal(args, reviewProposal)),
192
vscode.commands.registerCommand("oxygenhub.sealApproval", (...args: unknown[]) => withProposal(args, (repo, number) => runInTerminal(repo, `oxy seal --proposal ${hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })}`))),
193
vscode.commands.registerCommand("oxygenhub.land", (...args: unknown[]) => withProposal(args, (repo, number) => runInTerminal(repo, `oxy proposal land ${hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })}`))),
194
vscode.commands.registerCommand("oxygenhub.closeProposal", (...args: unknown[]) =>
195
withProposal(args, async (repo, number) => {
196
void vscode.env.openExternal(vscode.Uri.parse(hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })));
197
}),
198
),
199
vscode.commands.registerCommand("oxygenhub.createProposal", () => createProposal()),
200
vscode.commands.registerCommand("oxygenhub.newIssue", () => newIssue()),
95f69b4a Andy McCall 1 month ago ↶
201
vscode.commands.registerCommand("oxygenhub.issueFromSelection", () => issueFromSelection()),
f0b06dea Andy McCall 1 month ago
202
vscode.commands.registerCommand("oxygenhub.openIssue", async (node?: IssueNode) => node && showIssue(auth, node.repo, node.i)),
203
vscode.commands.registerCommand("oxygenhub.commentIssue", (...args: unknown[]) =>
204
withIssue(args, async (repo, number) => {
205
const body = await vscode.window.showInputBox({ prompt: `Comment on #${number}` });
206
if (body) {
207
await auth.clientFor(repo.base).commentIssue(repo.owner, repo.name, number, body);
208
issues.refresh();
209
}
210
}),
211
),
212
vscode.commands.registerCommand("oxygenhub.closeIssue", (...args: unknown[]) => withIssue(args, async (repo, number) => (await auth.clientFor(repo.base).setIssueState(repo.owner, repo.name, number, "closed"), issues.refresh()))),
213
vscode.commands.registerCommand("oxygenhub.reopenIssue", (...args: unknown[]) => withIssue(args, async (repo, number) => (await auth.clientFor(repo.base).setIssueState(repo.owner, repo.name, number, "open"), issues.refresh()))),
214
vscode.commands.registerCommand("oxygenhub.assignMe", (...args: unknown[]) =>
215
withIssue(args, async (repo, number) => {
092b8bf9 Andy McCall 1 month ago ↶
216
void vscode.window.showInformationMessage(`OxygenHub: assigning is on the hub for now: ${hubUrl(repo.base, repo.owner, repo.name, { kind: "issue", number })}`);
f0b06dea Andy McCall 1 month ago
217
}),
218
),
219
vscode.commands.registerCommand("oxygenhub.reply", (reply: vscode.CommentReply) => review.reply(reply)),
220
vscode.commands.registerCommand("oxygenhub.newThread", (reply: vscode.CommentReply) => review.reply(reply)),
221
vscode.commands.registerCommand("oxygenhub.resolveThread", (thread: vscode.CommentThread) => {
222
thread.state = thread.state === vscode.CommentThreadState.Resolved ? vscode.CommentThreadState.Unresolved : vscode.CommentThreadState.Resolved;
223
}),
224
);
225
bar.start();
b2dd082c Andy McCall 1 month ago ↶
226
inbox.start();
f0b06dea Andy McCall 1 month ago
227
}
228
229
function refreshAll(): void {
230
proposals.refresh();
f902d5b8 Andy McCall 1 month ago ↶
231
references.clear();
bf73998c Andy McCall 1 month ago ↶
232
refreshOpenDiffs();
f0b06dea Andy McCall 1 month ago
233
issues.refresh();
1cc0ff44 Andy McCall 1 month ago ↶
234
releases.refresh();
d21a8595 Andy McCall 23 days ago ↶
235
diffusion.refresh();
263de209 Andy McCall 1 month ago ↶
236
void flow.refresh();
f0b06dea Andy McCall 1 month ago
237
void bar.refresh();
b2dd082c Andy McCall 1 month ago ↶
238
void inbox.refresh();
f0b06dea Andy McCall 1 month ago
239
}
240
bf73998c Andy McCall 1 month ago ↶
241
/** Reload the sides of open proposal diffs, which may have gained changes. */
242
function refreshOpenDiffs(): void {
243
const seen = new Set<string>();
244
for (const doc of vscode.workspace.textDocuments) {
245
const r = refOf(doc.uri);
246
const key = r && `${r.base}/${r.owner}/${r.name}#${r.number}`;
247
if (r && key && !seen.has(key)) {
248
seen.add(key);
249
content.refresh(r.base, r.owner, r.name, r.number);
250
}
251
}
252
}
253
f0b06dea Andy McCall 1 month ago
254
async function guessHubs(): Promise<string[]> {
255
const out: string[] = [];
256
for (const r of oxygen?.repositories ?? []) {
257
for (const remote of await r.remotes().catch(() => [])) {
258
const m = /^(https?:\/\/[^/]+)\/[^/]+\/[^/]+\/?$/.exec(remote.url);
58889495 Andy McCall 1 month ago ↶
259
if (m && remote.ca) {
260
auth.setCa(m[1], remote.ca);
261
}
f0b06dea Andy McCall 1 month ago
262
if (m && !out.includes(m[1])) {
263
out.push(m[1]);
264
}
265
}
266
}
267
return out;
268
}
269
270
async function pickProposal(): Promise<ProposalNode | undefined> {
271
const items: { label: string; description: string; node: ProposalNode }[] = [];
272
for (const repo of await proposals.repos()) {
273
for (const p of await proposals.proposalsOf(repo).catch(() => [])) {
274
items.push({ label: `#${p.number} ${p.title}`, description: `${repo.owner}/${repo.name} · ${p.author} → ${p.target}`, node: { kind: "proposal", repo, p } });
275
}
276
}
277
return (await vscode.window.showQuickPick(items, { placeHolder: "Proposal" }))?.node;
278
}
279
280
/** Commands take a tree node, or the webview's `[base, owner, name, number]`. */
281
async function withProposal(args: unknown[], f: (repo: HubRepo, number: number) => Promise<void>): Promise<void> {
282
const a = args[0];
283
if (a && typeof a === "object" && (a as ProposalNode).kind === "proposal") {
284
const n = a as ProposalNode;
285
return f(n.repo, n.p.number);
286
}
287
if (Array.isArray(a) && a.length === 4) {
288
const [base, owner, name, number] = a as [string, string, string, number];
289
const repo = (await proposals.repos()).find((r) => r.base === base && r.owner === owner && r.name === name);
290
if (repo) {
291
return f(repo, number);
292
}
293
}
294
const n = await pickProposal();
295
if (n) {
296
return f(n.repo, n.p.number);
297
}
298
}
299
300
async function withIssue(args: unknown[], f: (repo: HubRepo, number: number) => Promise<void>): Promise<void> {
301
const a = args[0];
302
if (a && typeof a === "object" && (a as IssueNode).kind === "issue") {
303
const n = a as IssueNode;
304
return f(n.repo, n.i.number);
305
}
306
if (Array.isArray(a) && a.length === 4) {
307
const [base, owner, name, number] = a as [string, string, string, number];
308
const repo = (await proposals.repos()).find((r) => r.base === base && r.owner === owner && r.name === name);
309
if (repo) {
310
return f(repo, number);
311
}
312
}
313
}
314
315
function runInTerminal(repo: HubRepo, command: string): Promise<void> {
092b8bf9 Andy McCall 1 month ago ↶
316
const t = vscode.window.createTerminal({ name: "OxygenHub", cwd: repo.handle.root });
f0b06dea Andy McCall 1 month ago
317
t.show();
318
t.sendText(command);
319
return Promise.resolve();
320
}
321
322
/** Fetch the proposal's changes and switch to a local bookmark at its projected heads (spec §5). */
323
async function checkout(repo: HubRepo, number: number): Promise<void> {
324
const p = await auth.clientFor(repo.base).proposal(repo.owner, repo.name, number);
325
const prefix = vscode.workspace.getConfiguration("oxygenhub").get<string>("proposalBookmarkPrefix") ?? "proposal/";
326
const bookmark = `${prefix}${number}`;
327
try {
328
await repo.handle.json(["fetch", repo.remote, "--json"]);
329
const existing = repo.handle.bookmarks.some((b) => b.name === bookmark);
330
if (!existing) {
331
await repo.handle.json(["bookmark", "create", bookmark, ...p.projected_heads, "--json"]).catch(() => repo.handle.json(["bookmark", "create", bookmark, "--json"]));
332
}
333
await repo.handle.json(["bookmark", "set", bookmark, ...p.projected_heads, "--json"]).catch(() => undefined);
334
await repo.handle.json(["bookmark", "switch", bookmark, "--json"]);
335
await repo.handle.refresh();
336
await review.load(repo, number);
092b8bf9 Andy McCall 1 month ago ↶
337
void vscode.window.showInformationMessage(`OxygenHub: checked out #${number} as ${bookmark}; review threads are in place.`);
f0b06dea Andy McCall 1 month ago
338
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
339
void vscode.window.showErrorMessage(`OxygenHub: could not check out #${number}: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
340
}
341
}
342
343
async function reviewProposal(repo: HubRepo, number: number): Promise<void> {
344
const verdict = await vscode.window.showQuickPick(
345
[
346
{ label: "Comment", value: "comment" as const },
347
{ label: "Approve", value: "approve" as const, description: "then seal it with oxy so it counts" },
348
{ label: "Request changes", value: "changes" as const },
349
],
350
{ placeHolder: `Review #${number}` },
351
);
352
if (!verdict) {
353
return;
354
}
355
const body = (await vscode.window.showInputBox({ prompt: "Summary (optional)" })) ?? "";
356
try {
357
await auth.clientFor(repo.base).review(repo.owner, repo.name, number, verdict.value, body);
358
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
359
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
360
return;
361
}
362
proposals.refresh();
363
if (verdict.value === "approve") {
364
const pick = await vscode.window.showInformationMessage("Approval recorded. Seal it with your key so it counts?", "Seal now");
365
if (pick === "Seal now") {
366
await runInTerminal(repo, `oxy seal --proposal ${hubUrl(repo.base, repo.owner, repo.name, { kind: "proposal", number })}`);
367
}
368
}
369
}
370
371
async function createProposal(): Promise<void> {
372
const repos = await proposals.repos();
373
const repo = repos.length === 1 ? repos[0] : (await vscode.window.showQuickPick(repos.map((r) => ({ label: `${r.owner}/${r.name}`, r })), { placeHolder: "Repository" }))?.r;
374
if (!repo) {
375
return;
376
}
377
const source = repo.handle.currentBookmark;
378
if (!source) {
092b8bf9 Andy McCall 1 month ago ↶
379
void vscode.window.showWarningMessage("OxygenHub: no current bookmark.");
f0b06dea Andy McCall 1 month ago
380
return;
381
}
382
const remoteBookmarks = await auth.clientFor(repo.base).bookmarks(repo.owner, repo.name).catch(() => []);
383
const target = await vscode.window.showQuickPick(remoteBookmarks.filter((b) => b.name !== source).map((b) => b.name), { placeHolder: `Propose ${source} into…` });
384
if (!target) {
385
return;
386
}
387
const title = await vscode.window.showInputBox({ prompt: "Title", value: source });
388
if (!title) {
389
return;
390
}
391
try {
392
await repo.handle.json(["push", repo.remote, "--bookmark", source, "--json"]).catch(() => undefined);
393
const p = await auth.clientFor(repo.base).createProposal(repo.owner, repo.name, { title, body: "", target, source, draft: false });
394
proposals.refresh();
395
void vscode.env.openExternal(vscode.Uri.parse(p.url));
396
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
397
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
398
}
399
}
400
401
async function newIssue(): Promise<void> {
402
const repos = await proposals.repos();
403
const repo = repos.length === 1 ? repos[0] : (await vscode.window.showQuickPick(repos.map((r) => ({ label: `${r.owner}/${r.name}`, r })), { placeHolder: "Repository" }))?.r;
404
if (!repo) {
405
return;
406
}
407
const title = await vscode.window.showInputBox({ prompt: "Issue title" });
408
if (!title) {
409
return;
410
}
411
const body = (await vscode.window.showInputBox({ prompt: "Description (optional)" })) ?? "";
412
try {
413
const i = await auth.clientFor(repo.base).createIssue(repo.owner, repo.name, title, body);
414
issues.refresh();
092b8bf9 Andy McCall 1 month ago ↶
415
void vscode.window.showInformationMessage(`OxygenHub: opened #${i.number}.`);
f0b06dea Andy McCall 1 month ago
416
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
417
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
f0b06dea Andy McCall 1 month ago
418
}
419
}
420
95f69b4a Andy McCall 1 month ago ↶
421
/**
422
* An issue from the editor (spec §8.1): the selection, or the current line,
423
* quoted under a permalink to it on the hub. A TODO line suggests the title
424
* and afterwards records the issue's number.
425
*/
426
async function issueFromSelection(): Promise<void> {
427
const editor = vscode.window.activeTextEditor;
428
if (!editor || editor.document.uri.scheme !== "file") {
092b8bf9 Andy McCall 1 month ago ↶
429
void vscode.window.showWarningMessage("OxygenHub: open a file of the repository first.");
95f69b4a Andy McCall 1 month ago ↶
430
return;
431
}
432
const doc = editor.document;
433
const file = doc.uri.fsPath;
434
const repo = (await proposals.repos()).filter((r) => file.startsWith(r.handle.root + "/") || file.startsWith(r.handle.root + "\\")).sort((a, b) => b.handle.root.length - a.handle.root.length)[0];
435
if (!repo) {
092b8bf9 Andy McCall 1 month ago ↶
436
void vscode.window.showWarningMessage("OxygenHub: this file is not in a repository with a hub remote.");
95f69b4a Andy McCall 1 month ago ↶
437
return;
438
}
439
const sel = editor.selection;
440
const startLine = sel.start.line;
441
const endLine = sel.isEmpty ? sel.start.line : sel.end.character === 0 && sel.end.line > sel.start.line ? sel.end.line - 1 : sel.end.line;
442
const code = doc.getText(new vscode.Range(startLine, 0, endLine, doc.lineAt(endLine).text.length));
443
const todoLine = [...Array(endLine - startLine + 1).keys()].map((i) => startLine + i).find((l) => todoTitle(doc.lineAt(l).text) !== undefined);
444
const title = await vscode.window.showInputBox({ prompt: "Issue title", value: todoLine !== undefined ? todoTitle(doc.lineAt(todoLine).text) : "" });
445
if (!title) {
446
return;
447
}
448
// Pin the link to the head when the bookmark has one; otherwise follow the bookmark.
449
const bookmark = repo.handle.bookmarks.find((b) => b.name === repo.handle.currentBookmark);
450
const pinned = bookmark?.heads.length === 1 && !doc.isDirty;
451
const reference = pinned ? bookmark!.heads[0] : (repo.handle.currentBookmark ?? "main");
452
const path = file.slice(repo.handle.root.length + 1).split("\\").join("/");
453
const permalink = hubUrl(repo.base, repo.owner, repo.name, { kind: "file", reference, path, line: startLine + 1, endLine: endLine + 1 });
454
const body = issueBody({ permalink, path, startLine: startLine + 1, endLine: endLine + 1, language: doc.languageId === "plaintext" ? "" : doc.languageId, code, pinned });
455
try {
456
const client = auth.clientFor(repo.base);
457
const issue = await client.createIssue(repo.owner, repo.name, title, body);
458
issues.refresh();
459
references.clear();
460
if (todoLine !== undefined) {
461
const line = doc.lineAt(todoLine);
462
const annotated = annotateTodo(line.text, issue.number);
463
if (annotated !== line.text) {
092b8bf9 Andy McCall 1 month ago ↶
464
const pick = await vscode.window.showInformationMessage(`OxygenHub: opened #${issue.number}. Record it in the TODO?`, "Add (#" + issue.number + ")");
95f69b4a Andy McCall 1 month ago ↶
465
if (pick) {
466
await editor.edit((e) => e.replace(line.range, annotated));
467
}
468
}
469
}
470
await showIssue(auth, repo, issue);
471
} catch (e) {
092b8bf9 Andy McCall 1 month ago ↶
472
void vscode.window.showErrorMessage(`OxygenHub: ${(e as Error).message}`);
95f69b4a Andy McCall 1 month ago ↶
473
}
474
}
475
f0b06dea Andy McCall 1 month ago
476
export function deactivate(): void {}