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