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