bed495a9 Andy McCall 1 month ago ↶
1
// Activation: find oxy, find repositories, wire the source control view,
2
// decorations, the content provider, and the commands (spec §3).
3
4
import * as vscode from "vscode";
5
import { basename } from "node:path";
6
import { Oxy, OxyError, findBinary } from "./oxy";
d2c9a584 Andy McCall 1 month ago ↶
7
import { Repository, OxyResourceState, SubrepoState } from "./repository";
8
import { BisectProvider, TrustProvider } from "./panels";
ab81294a Andy McCall 1 month ago ↶
9
import { OxyContentProvider, SCHEME, baseUri, oxyUri } from "./contentProvider";
bed495a9 Andy McCall 1 month ago ↶
10
import { OxyDecorationProvider } from "./decorations";
ab81294a Andy McCall 1 month ago ↶
11
import { OperationItem, OperationsProvider, timeAgo } from "./operations";
12
import { BlameController } from "./blame";
13
import { HistoryPanel } from "./history";
25103ddd Andy McCall 1 month ago ↶
14
import { ConflictCodeLensProvider, acceptSides, keepCandidate, markResolved } from "./conflicts";
bed495a9 Andy McCall 1 month ago ↶
15
16
const MIN_VERSION = { format: 2, protocol: 1 };
17
18
let output: vscode.OutputChannel;
19
let oxy: Oxy;
20
let contentProvider: OxyContentProvider;
21
let decorations: OxyDecorationProvider;
35d56b58 Andy McCall 1 month ago ↶
22
let operations: OperationsProvider;
ab81294a Andy McCall 1 month ago ↶
23
let blame: BlameController;
25103ddd Andy McCall 1 month ago ↶
24
let conflictLenses: ConflictCodeLensProvider;
d2c9a584 Andy McCall 1 month ago ↶
25
let bisectView: BisectProvider;
26
let trustView: TrustProvider;
bed495a9 Andy McCall 1 month ago ↶
27
const repositories = new Map<string, Repository>();
28
29
export async function activate(context: vscode.ExtensionContext): Promise<void> {
30
output = vscode.window.createOutputChannel("Oxygen");
31
context.subscriptions.push(output);
810a1c53 Andy McCall 1 month ago ↶
32
output.appendLine(`Oxygen extension ${context.extension.packageJSON.version} activating; folders: ${(vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath).join(", ") || "(none)"}`);
bed495a9 Andy McCall 1 month ago ↶
33
oxy = new Oxy(findBinary(vscode.workspace.getConfiguration("oxygen").get<string>("path")), (line) => output.appendLine(line));
34
35
try {
36
const v = await oxy.version();
37
output.appendLine(`oxy ${v.version} (format ${v.format}, protocol ${v.protocol}) at ${oxy.binary}`);
38
if (v.format < MIN_VERSION.format || v.protocol < MIN_VERSION.protocol) {
39
void vscode.window.showWarningMessage(`Oxygen: oxy ${v.version} is older than this extension supports.`);
40
}
41
} catch (e) {
42
const pick = await vscode.window.showErrorMessage(`Oxygen: could not run oxy (${(e as Error).message}). Set "oxygen.path" or put oxy on your PATH.`, "Open Settings");
43
if (pick === "Open Settings") {
44
void vscode.commands.executeCommand("workbench.action.openSettings", "oxygen.path");
45
}
46
return;
47
}
48
49
contentProvider = new OxyContentProvider(oxy);
50
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(SCHEME, contentProvider));
51
decorations = new OxyDecorationProvider(() => [...repositories.values()]);
52
context.subscriptions.push(decorations);
35d56b58 Andy McCall 1 month ago ↶
53
operations = new OperationsProvider(() => [...repositories.values()]);
54
context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygen.operations", operations));
ab81294a Andy McCall 1 month ago ↶
55
blame = new BlameController(() => [...repositories.values()]);
56
context.subscriptions.push(blame);
25103ddd Andy McCall 1 month ago ↶
57
conflictLenses = new ConflictCodeLensProvider(() => [...repositories.values()]);
58
context.subscriptions.push(vscode.languages.registerCodeLensProvider({ scheme: "file" }, conflictLenses));
d2c9a584 Andy McCall 1 month ago ↶
59
bisectView = new BisectProvider(() => [...repositories.values()]);
60
trustView = new TrustProvider(() => [...repositories.values()]);
61
context.subscriptions.push(vscode.window.registerTreeDataProvider("oxygen.bisect", bisectView), vscode.window.registerTreeDataProvider("oxygen.trust", trustView));
bed495a9 Andy McCall 1 month ago ↶
62
63
context.subscriptions.push(
64
vscode.commands.registerCommand("oxygen.refresh", async () => {
65
await Promise.all([...repositories.values()].map((r) => r.refresh()));
66
}),
67
vscode.commands.registerCommand("oxygen.showOutput", () => output.show()),
68
vscode.commands.registerCommand("oxygen.openFile", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)),
69
vscode.commands.registerCommand("oxygen.openChanges", (state?: OxyResourceState) => state && openChanges(state)),
70
vscode.commands.registerCommand("oxygen.openBaseVersion", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", baseUri(state.repository.root, state.path))),
71
vscode.commands.registerCommand("oxygen.commit", (arg?: Repository | vscode.SourceControl) => commit(arg)),
72
vscode.commands.registerCommand("oxygen.showLog", () => showLog()),
810a1c53 Andy McCall 1 month ago ↶
73
vscode.commands.registerCommand("oxygen.init", () => initRepository()),
35d56b58 Andy McCall 1 month ago ↶
74
vscode.commands.registerCommand("oxygen.discard", (state?: OxyResourceState, states?: OxyResourceState[]) => discard(states?.length ? states : state ? [state] : [])),
75
vscode.commands.registerCommand("oxygen.commitSelected", (state?: OxyResourceState, states?: OxyResourceState[]) => commitSelected(states?.length ? states : state ? [state] : [])),
76
vscode.commands.registerCommand("oxygen.commitHunks", (arg?: Repository | vscode.SourceControl) => commitHunks(arg)),
77
vscode.commands.registerCommand("oxygen.amend", (arg?: Repository | vscode.SourceControl) => amend(arg)),
78
vscode.commands.registerCommand("oxygen.undo", (arg?: Repository | vscode.SourceControl) => undo(arg, 1)),
79
vscode.commands.registerCommand("oxygen.undoTo", (item?: OperationItem) => item && undo(item.repository, item.stepsBack)),
80
vscode.commands.registerCommand("oxygen.refreshOperations", () => operations.refresh()),
c8f95589 Andy McCall 1 month ago ↶
81
vscode.commands.registerCommand("oxygen.switchBookmark", (arg?: Repository | vscode.SourceControl) => switchBookmark(arg)),
82
vscode.commands.registerCommand("oxygen.createBookmark", (arg?: Repository | vscode.SourceControl) => createBookmark(arg)),
83
vscode.commands.registerCommand("oxygen.mergeBookmark", (arg?: Repository | vscode.SourceControl) => mergeBookmark(arg)),
84
vscode.commands.registerCommand("oxygen.deleteBookmark", (arg?: Repository | vscode.SourceControl) => deleteBookmark(arg)),
85
vscode.commands.registerCommand("oxygen.fetch", (arg?: Repository | vscode.SourceControl) => fetch(arg)),
86
vscode.commands.registerCommand("oxygen.pull", (arg?: Repository | vscode.SourceControl) => pull(arg)),
87
vscode.commands.registerCommand("oxygen.push", (arg?: Repository | vscode.SourceControl) => push(arg, false)),
88
vscode.commands.registerCommand("oxygen.pushBookmark", (arg?: Repository | vscode.SourceControl) => push(arg, true)),
89
vscode.commands.registerCommand("oxygen.sync", (arg?: Repository | vscode.SourceControl) => sync(arg)),
ab81294a Andy McCall 1 month ago ↶
90
vscode.commands.registerCommand("oxygen.showHistory", (arg?: Repository | vscode.SourceControl) => {
91
const repo = repositoryFor(arg);
92
if (repo) {
93
HistoryPanel.show(repo, (hash) => void showChange(repo.root, hash));
94
}
95
}),
96
vscode.commands.registerCommand("oxygen.fileHistory", (uri?: vscode.Uri) => fileHistory(uri ?? vscode.window.activeTextEditor?.document.uri)),
97
vscode.commands.registerCommand("oxygen.toggleBlame", () => blame.toggle()),
98
vscode.commands.registerCommand("oxygen.showChange", (root?: string, hash?: string) => root && hash && showChange(root, hash)),
25103ddd Andy McCall 1 month ago ↶
99
vscode.commands.registerCommand("oxygen.acceptSides", (uri: vscode.Uri, index: number, keep: number[]) => acceptSides(uri, index, keep).catch(showError)),
100
vscode.commands.registerCommand("oxygen.keepCandidate", (state?: OxyResourceState) => state && keepCandidate(state.repository, state.path).catch(showError)),
101
vscode.commands.registerCommand("oxygen.markResolved", (arg?: Repository | vscode.SourceControl) => {
102
const repo = repositoryFor(arg);
103
return repo && markResolved(repo).catch(showError);
104
}),
105
vscode.commands.registerCommand("oxygen.openConflict", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)),
d2c9a584 Andy McCall 1 month ago ↶
106
vscode.commands.registerCommand("oxygen.openSubrepo", (state?: SubrepoState) => state && openSubrepo(state)),
107
vscode.commands.registerCommand("oxygen.updateSubrepo", (state?: SubrepoState) => state && updateSubrepos(state.repository, true, [state.info.path])),
108
vscode.commands.registerCommand("oxygen.updateSubrepos", (arg?: Repository | vscode.SourceControl) => {
109
const repo = repositoryFor(arg);
110
return repo && updateSubrepos(repo, true, []);
111
}),
112
vscode.commands.registerCommand("oxygen.bisectStart", (arg?: Repository) => bisect(arg, (r) => r.oxy.bisectStart(r.root).then(() => "Started; mark a good and a bad change"))),
113
vscode.commands.registerCommand("oxygen.bisectGood", (arg?: Repository, changes?: string[]) => bisectMark(arg, "good", changes)),
114
vscode.commands.registerCommand("oxygen.bisectBad", (arg?: Repository, changes?: string[]) => bisectMark(arg, "bad", changes)),
115
vscode.commands.registerCommand("oxygen.bisectSkip", (arg?: Repository) => bisect(arg, (r) => r.oxy.bisectMark(r.root, "skip"))),
116
vscode.commands.registerCommand("oxygen.bisectRun", (arg?: Repository) => bisectRun(arg)),
117
vscode.commands.registerCommand("oxygen.bisectReset", (arg?: Repository) => bisect(arg, (r) => r.oxy.bisectReset(r.root).then(() => "Bisect reset; working copy restored"))),
118
vscode.commands.registerCommand("oxygen.refreshBisect", () => bisectView.refresh()),
119
vscode.commands.registerCommand("oxygen.refreshTrust", () => trustView.refresh()),
120
vscode.commands.registerCommand("oxygen.verify", (arg?: Repository | vscode.SourceControl) => verify(arg)),
121
vscode.commands.registerCommand("oxygen.tags", (arg?: Repository | vscode.SourceControl) => tags(arg)),
122
vscode.commands.registerCommand("oxygen.createTag", (arg?: Repository | vscode.SourceControl) => createTag(arg)),
123
vscode.commands.registerCommand("oxygen.addWorkspace", (arg?: Repository | vscode.SourceControl) => addWorkspace(arg)),
124
vscode.commands.registerCommand("oxygen.clone", () => cloneRepository()),
bed495a9 Andy McCall 1 month ago ↶
125
vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()),
126
vscode.workspace.onDidChangeConfiguration((e) => {
127
if (e.affectsConfiguration("oxygen.path")) {
128
void vscode.window.showInformationMessage("Oxygen: reload the window to use the new oxy path.");
129
}
130
if (e.affectsConfiguration("oxygen")) {
131
decorations.refresh();
132
for (const r of repositories.values()) {
133
r.scheduleRefresh();
134
}
135
}
136
}),
137
);
138
context.subscriptions.push({ dispose: () => repositories.forEach((r) => r.dispose()) });
139
140
await discover();
141
}
142
143
export function deactivate(): void {
144
for (const r of repositories.values()) {
145
r.dispose();
146
}
147
repositories.clear();
148
}
149
150
/** One Repository per distinct oxy root among the workspace folders. */
151
async function discover(): Promise<void> {
152
const wanted = new Map<string, vscode.WorkspaceFolder>();
153
for (const folder of vscode.workspace.workspaceFolders ?? []) {
154
const info = await oxy.root(folder.uri.fsPath);
155
if (info) {
156
wanted.set(info.root, folder);
157
}
158
}
159
for (const [root, repo] of repositories) {
160
if (!wanted.has(root)) {
161
repo.dispose();
162
repositories.delete(root);
163
}
164
}
810a1c53 Andy McCall 1 month ago ↶
165
if (wanted.size === 0 && (vscode.workspace.workspaceFolders ?? []).length > 0) {
166
output.appendLine("no Oxygen repository in the open folders (oxy root found none)");
167
}
bed495a9 Andy McCall 1 month ago ↶
168
for (const [root, folder] of wanted) {
169
if (!repositories.has(root)) {
170
const repo = new Repository(oxy, root, folder);
25103ddd Andy McCall 1 month ago ↶
171
repo.onDidChangeStatus((status) => {
bed495a9 Andy McCall 1 month ago ↶
172
decorations.refresh();
173
contentProvider.invalidate(root);
35d56b58 Andy McCall 1 month ago ↶
174
operations.refresh();
ab81294a Andy McCall 1 month ago ↶
175
blame.invalidate(root);
176
HistoryPanel.refresh(root);
25103ddd Andy McCall 1 month ago ↶
177
conflictLenses.refresh();
d2c9a584 Andy McCall 1 month ago ↶
178
bisectView.refresh();
179
trustView.refresh();
25103ddd Andy McCall 1 month ago ↶
180
// The commit action reads as a resolution while conflicts are resolved but uncommitted.
181
const resolved = status.conflicted && status.conflicts.length > 0 && status.conflicts.every((c) => c.resolved === true);
182
repo.sourceControl.acceptInputCommand = { command: "oxygen.commit", title: resolved ? "Commit resolution" : "Commit", arguments: [repo] };
183
repo.sourceControl.inputBox.placeholder = resolved ? "Describe the resolution (Ctrl+Enter to commit)" : "Message (Ctrl+Enter to commit)";
bed495a9 Andy McCall 1 month ago ↶
184
});
185
repositories.set(root, repo);
186
output.appendLine(`repository ${root}`);
187
await repo.refresh();
188
}
189
}
190
void vscode.commands.executeCommand("setContext", "oxygen.hasRepository", repositories.size > 0);
191
}
192
193
function repositoryFor(arg: Repository | vscode.SourceControl | undefined): Repository | undefined {
194
if (arg instanceof Repository) {
195
return arg;
196
}
197
if (arg && "rootUri" in arg && arg.rootUri) {
198
return repositories.get(arg.rootUri.fsPath);
199
}
200
const active = vscode.window.activeTextEditor?.document.uri;
201
for (const repo of repositories.values()) {
202
if (active && repo.relativePath(active) !== undefined) {
203
return repo;
204
}
205
}
206
return repositories.size === 1 ? [...repositories.values()][0] : undefined;
207
}
208
209
async function openChanges(state: OxyResourceState): Promise<void> {
210
const repo = state.repository;
211
const title = `${basename(state.path)} (base ↔ working copy)`;
212
if (state.status === "Added") {
213
await vscode.commands.executeCommand("vscode.open", state.resourceUri);
214
return;
215
}
216
if (state.status === "Removed") {
217
await vscode.commands.executeCommand("vscode.open", baseUri(repo.root, state.path));
218
return;
219
}
220
await vscode.commands.executeCommand("vscode.diff", baseUri(repo.root, state.path), state.resourceUri, title);
221
}
222
223
async function commit(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
224
const repo = repositoryFor(arg);
225
if (!repo) {
226
void vscode.window.showErrorMessage("Oxygen: no repository for the active file.");
227
return;
228
}
229
let message = repo.sourceControl.inputBox.value.trim();
230
if (!message) {
231
message = (await vscode.window.showInputBox({ prompt: "Commit message", placeHolder: "What did you change?" }))?.trim() ?? "";
232
}
233
if (!message) {
234
return;
235
}
236
try {
237
await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message));
238
repo.sourceControl.inputBox.value = "";
239
await repo.refresh();
240
} catch (e) {
241
showError(e);
242
}
243
}
244
810a1c53 Andy McCall 1 month ago ↶
245
async function initRepository(): Promise<void> {
246
const folders = vscode.workspace.workspaceFolders ?? [];
247
if (folders.length === 0) {
248
void vscode.window.showErrorMessage("Oxygen: open a folder first.");
249
return;
250
}
251
const folder = folders.length === 1 ? folders[0] : await vscode.window.showWorkspaceFolderPick({ placeHolder: "Folder to initialize as an Oxygen repository" });
252
if (!folder) {
253
return;
254
}
255
try {
256
await oxy.check(folder.uri.fsPath, ["init"]);
257
await discover();
258
void vscode.window.showInformationMessage(`Oxygen: initialized ${folder.name}.`);
259
} catch (e) {
260
showError(e);
261
}
262
}
263
35d56b58 Andy McCall 1 month ago ↶
264
async function discard(states: OxyResourceState[]): Promise<void> {
265
if (states.length === 0) {
266
return;
267
}
268
const repo = states[0].repository;
269
const names = states.length === 1 ? basename(states[0].path) : `${states.length} files`;
270
const pick = await vscode.window.showWarningMessage(`Discard changes in ${names}? This cannot be undone with oxy undo.`, { modal: true }, "Discard");
271
if (pick !== "Discard") {
272
return;
273
}
274
try {
275
await repo.oxy.discard(repo.root, states.map((s) => s.path));
276
await repo.refresh();
277
} catch (e) {
278
showError(e);
279
}
280
}
281
282
async function commitSelected(states: OxyResourceState[]): Promise<void> {
283
if (states.length === 0) {
284
return;
285
}
286
const repo = states[0].repository;
287
const message = await askMessage(repo, `Commit ${states.length === 1 ? basename(states[0].path) : `${states.length} files`}`);
288
if (!message) {
289
return;
290
}
291
try {
292
await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message, states.map((s) => s.path)));
293
repo.sourceControl.inputBox.value = "";
294
await repo.refresh();
295
} catch (e) {
296
showError(e);
297
}
298
}
299
300
async function commitHunks(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
301
const repo = repositoryFor(arg);
302
if (!repo) {
303
return;
304
}
305
let hunks;
306
try {
307
hunks = await repo.oxy.hunks(repo.root);
308
} catch (e) {
309
showError(e);
310
return;
311
}
312
if (hunks.length === 0) {
313
void vscode.window.showInformationMessage("Oxygen: nothing to commit.");
314
return;
315
}
316
const picks = await vscode.window.showQuickPick(
317
hunks.map((h) => ({
318
label: `$(diff) ${h.path}`,
319
description: `#${h.index}`,
320
detail: h.text.split("\n").filter((l) => l.startsWith("+") || l.startsWith("-") || l.startsWith("A ") || l.startsWith("D ") || l.startsWith("R ")).slice(0, 3).join(" ").slice(0, 160),
321
hunk: h,
322
picked: false,
323
})),
324
{ canPickMany: true, placeHolder: "Choose the hunks to commit; anything a chosen hunk needs comes along" },
325
);
326
if (!picks || picks.length === 0) {
327
return;
328
}
329
const message = await askMessage(repo, `Commit ${picks.length} hunk${picks.length === 1 ? "" : "s"}`);
330
if (!message) {
331
return;
332
}
333
try {
334
await repo.oxy.commitHunks(repo.root, message, picks.map((p) => p.hunk.index));
335
repo.sourceControl.inputBox.value = "";
336
await repo.refresh();
337
} catch (e) {
338
showError(e);
339
}
340
}
341
342
async function amend(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
343
const repo = repositoryFor(arg);
344
if (!repo) {
345
return;
346
}
347
const message = repo.sourceControl.inputBox.value.trim();
348
try {
349
await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Amending" }, () => repo.oxy.amend(repo.root, message || undefined));
350
repo.sourceControl.inputBox.value = "";
351
await repo.refresh();
352
} catch (e) {
353
showError(e);
354
}
355
}
356
357
async function undo(arg: Repository | vscode.SourceControl | undefined, steps: number): Promise<void> {
358
const repo = repositoryFor(arg);
359
if (!repo || steps <= 0) {
360
return;
361
}
362
if (steps > 1) {
363
const pick = await vscode.window.showWarningMessage(`Undo the last ${steps} operations?`, { modal: true }, "Undo");
364
if (pick !== "Undo") {
365
return;
366
}
367
}
368
try {
369
let last = "";
370
for (let i = 0; i < steps; i++) {
371
last = await repo.oxy.undo(repo.root);
372
}
373
await repo.refresh();
374
vscode.window.setStatusBarMessage(`$(discard) ${last || "Undone"}`, 4000);
375
} catch (e) {
376
showError(e);
377
}
378
}
379
d2c9a584 Andy McCall 1 month ago ↶
380
// ---------------------------------------------------------------- subrepos, bisect, trust, tags, workspaces, clone
381
382
async function openSubrepo(state: SubrepoState): Promise<void> {
383
if (!state.info.actual) {
384
const pick = await vscode.window.showInformationMessage(`${state.info.path} is not checked out yet.`, "Update --init");
385
if (pick) {
386
await updateSubrepos(state.repository, true, [state.info.path]);
387
}
388
return;
389
}
390
const pick = await vscode.window.showQuickPick(
391
[
392
{ label: "$(folder-opened) Open in new window", action: "window" },
393
{ label: "$(cloud-download) Update to the pin", action: "update", description: state.info.at_pin ? "already at pin" : "moves the nested repository" },
394
],
395
{ placeHolder: `${state.info.path} · ${state.decorations.tooltip}` },
396
);
397
if (pick?.action === "window") {
398
await vscode.commands.executeCommand("vscode.openFolder", state.resourceUri, { forceNewWindow: true });
399
} else if (pick?.action === "update") {
400
await updateSubrepos(state.repository, true, [state.info.path]);
401
}
402
}
403
404
async function updateSubrepos(repo: Repository, init: boolean, paths: string[]): Promise<void> {
405
try {
406
const out = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Updating subrepos" }, () => repo.oxy.submoduleUpdate(repo.root, init, paths));
407
await repo.refresh();
408
const lines = out.trim().split("\n").filter(Boolean);
409
if (lines.some((l) => /missing|uncommitted|✖|x /.test(l))) {
410
void vscode.window.showWarningMessage(`Oxygen: ${lines.join("; ")}`);
411
} else {
412
vscode.window.setStatusBarMessage(`$(check) ${lines.join("; ") || "Subrepos up to date"}`, 5000);
413
}
414
} catch (e) {
415
showError(e);
416
}
417
}
418
419
async function bisect(arg: Repository | undefined, run: (repo: Repository) => Promise<string>): Promise<void> {
420
const repo = arg instanceof Repository ? arg : repositoryFor(undefined);
421
if (!repo) {
422
return;
423
}
424
try {
425
const out = await run(repo);
426
await repo.refresh();
427
vscode.window.setStatusBarMessage(`$(search) ${out.split("\n")[0]}`, 5000);
428
} catch (e) {
429
showError(e);
430
}
431
}
432
433
async function bisectMark(arg: Repository | undefined, verdict: "good" | "bad", changes?: string[]): Promise<void> {
434
const repo = arg instanceof Repository ? arg : repositoryFor(undefined);
435
if (!repo) {
436
return;
437
}
438
let picked = changes;
439
if (picked === undefined) {
440
const status = await repo.oxy.bisectStatus(repo.root).catch(() => null);
441
if (!status || status.state === "need-marks") {
442
const value = await vscode.window.showInputBox({ prompt: `A change, bookmark or tag known to be ${verdict}`, placeHolder: verdict === "good" ? "v1.2 or a ChangeId prefix" : "main" });
443
if (!value?.trim()) {
444
return;
445
}
446
picked = value.trim().split(/\s+/);
447
} else {
448
picked = [];
449
}
450
}
451
await bisect(repo, (r) => r.oxy.bisectMark(r.root, verdict, picked));
452
}
453
454
async function bisectRun(arg: Repository | undefined): Promise<void> {
455
const repo = arg instanceof Repository ? arg : repositoryFor(undefined);
456
if (!repo) {
457
return;
458
}
459
const value = await vscode.window.showInputBox({ prompt: "Command to run for each step (exit 0 good, 125 skip, anything else bad)", placeHolder: "make test" });
460
if (!value?.trim()) {
461
return;
462
}
463
await bisect(repo, (r) => Promise.resolve(vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Oxygen: bisecting", cancellable: false }, () => r.oxy.bisectRun(r.root, ["sh", "-c", value.trim()]))));
464
}
465
466
async function verify(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
467
const repo = repositoryFor(arg);
468
if (!repo) {
469
return;
470
}
471
try {
472
const results = await repo.oxy.verify(repo.root);
473
const icon = (s: string, revoked: boolean) => (s === "invalid" ? "$(error)" : s === "unsigned" ? "$(circle-outline)" : revoked ? "$(warning)" : "$(verified)");
474
const pick = await vscode.window.showQuickPick(
475
results.map((r) => ({
476
label: `${icon(r.status, r.revoked)} ${r.change_id.slice(0, 8)} ${r.status}${r.revoked ? " (key revoked)" : ""}`,
477
description: r.name ?? "",
478
detail: r.reason ?? undefined,
479
hash: r.hash,
480
})),
481
{ placeHolder: `${results.length} changes; ${results.filter((r) => r.status === "invalid").length} invalid`, matchOnDescription: true },
482
);
483
if (pick) {
484
await showChange(repo.root, pick.hash);
485
}
486
} catch (e) {
487
showError(e);
488
}
489
}
490
491
async function tags(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
492
const repo = repositoryFor(arg);
493
if (!repo) {
494
return;
495
}
496
try {
497
const list = await repo.oxy.tags(repo.root);
498
if (list.length === 0) {
499
const pick = await vscode.window.showInformationMessage("Oxygen: no tags yet.", "Create tag");
500
if (pick) {
501
await createTag(repo);
502
}
503
return;
504
}
505
const pick = await vscode.window.showQuickPick(
506
list.map((t) => ({ label: `$(tag) ${t.name}`, description: `${t.heads.length} head${t.heads.length === 1 ? "" : "s"}${t.signer ? ` · signed by ${t.signer}` : ""}`, detail: t.message || undefined, tag: t })),
507
{ placeHolder: "Check out a tag's state" },
508
);
509
if (!pick) {
510
return;
511
}
512
await repo.oxy.checkout(repo.root, [pick.tag.name]);
513
await repo.refresh();
514
} catch (e) {
515
if (e instanceof OxyError && e.message.includes("has changes")) {
516
void vscode.window.showWarningMessage("Oxygen: commit or discard your changes before checking out a tag.");
517
return;
518
}
519
showError(e);
520
}
521
}
522
523
async function createTag(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
524
const repo = repositoryFor(arg);
525
if (!repo) {
526
return;
527
}
528
const name = await vscode.window.showInputBox({ prompt: "Tag name (tags the current bookmark's heads)" });
529
if (!name?.trim()) {
530
return;
531
}
532
const message = (await vscode.window.showInputBox({ prompt: "Tag message (optional)" })) ?? "";
533
try {
534
await repo.oxy.tagCreate(repo.root, name.trim(), [], message);
535
await repo.refresh();
536
vscode.window.setStatusBarMessage(`$(tag) Tagged ${name.trim()}`, 4000);
537
} catch (e) {
538
showError(e);
539
}
540
}
541
542
async function addWorkspace(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
543
const repo = repositoryFor(arg);
544
if (!repo) {
545
return;
546
}
547
const folders = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, openLabel: "Create workspace here", title: "Folder for the new workspace (must be empty or absent)" });
548
if (!folders?.[0]) {
549
return;
550
}
551
try {
552
await repo.oxy.workspaceAdd(repo.root, folders[0].fsPath);
553
const pick = await vscode.window.showInformationMessage(`Oxygen: workspace created at ${folders[0].fsPath}.`, "Open in new window");
554
if (pick) {
555
await vscode.commands.executeCommand("vscode.openFolder", folders[0], { forceNewWindow: true });
556
}
557
} catch (e) {
558
showError(e);
559
}
560
}
561
562
async function cloneRepository(): Promise<void> {
563
const url = await vscode.window.showInputBox({ prompt: "Repository to clone (path or https URL)", placeHolder: "https://host:8890/project" });
564
if (!url?.trim()) {
565
return;
566
}
567
const parent = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, openLabel: "Clone into this folder" });
568
if (!parent?.[0]) {
569
return;
570
}
571
const name = url.trim().replace(/\/+$/, "").split("/").pop()?.replace(/\.oxy$/, "") || "repository";
572
const dest = vscode.Uri.joinPath(parent[0], name);
573
try {
574
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: `Oxygen: cloning ${name}` }, () => oxy.clone(parent[0].fsPath, url.trim(), dest.fsPath));
575
const pick = await vscode.window.showInformationMessage(`Oxygen: cloned into ${dest.fsPath}.`, "Open", "Open in new window");
576
if (pick) {
577
await vscode.commands.executeCommand("vscode.openFolder", dest, { forceNewWindow: pick === "Open in new window" });
578
}
579
} catch (e) {
580
showError(e);
581
}
582
}
583
ab81294a Andy McCall 1 month ago ↶
584
// ---------------------------------------------------------------- history
585
586
async function showChange(root: string, hash: string): Promise<void> {
587
const repo = repositories.get(root);
588
if (!repo) {
589
return;
590
}
591
let info;
592
try {
593
info = await repo.oxy.show(repo.root, `#${hash.slice(0, 12)}`);
594
} catch (e) {
595
showError(e);
596
return;
597
}
598
const subject = info.message.split("\n")[0];
599
type Item = vscode.QuickPickItem & { action: string; path?: string };
600
const items: Item[] = [
601
{ label: "$(diff) Show diff", description: `${info.files.length} file${info.files.length === 1 ? "" : "s"}`, action: "diff" },
602
{ label: "$(arrow-right) Check out this state", description: "the change and its dependencies", action: "checkout" },
603
{ label: "$(edit) Describe…", description: "change the message", action: "describe" },
604
{ label: "$(tag) Tag…", action: "tag" },
605
{ label: "$(trash) Abandon", description: "drop the change; its content stays on disk if the working copy sat on it", action: "abandon" },
606
{ label: "$(copy) Copy ChangeId", action: "copy" },
607
...info.files.map((f) => ({ label: `$(file) ${f.path}`, description: `${f.kind}${f.from ? ` from ${f.from}` : ""}`, action: "file", path: f.path })),
608
];
609
const pick = await vscode.window.showQuickPick(items, {
610
title: `${info.change_id.slice(0, 8)} ${subject}`,
611
placeHolder: `${info.author} · ${timeAgo(info.when_ms)} · ${info.signature?.status ?? "unsigned"}${info.signature?.revoked ? " (key revoked)" : ""} · ${info.deps.length} dependenc${info.deps.length === 1 ? "y" : "ies"}`,
612
matchOnDescription: true,
613
});
614
if (!pick) {
615
return;
616
}
617
try {
618
switch (pick.action) {
619
case "diff":
620
case "file": {
621
const text = await repo.oxy.diffText(repo.root, `#${hash.slice(0, 12)}`);
622
const body = `# ${info.change_id.slice(0, 8)} ${subject}\n# ${info.author} · ${new Date(info.when_ms).toLocaleString()}\n\n${text}`;
623
const doc = await vscode.workspace.openTextDocument({ content: body, language: "diff" });
624
await vscode.window.showTextDocument(doc, { preview: true });
625
return;
626
}
627
case "checkout":
628
await repo.oxy.checkout(repo.root, [`#${hash.slice(0, 12)}`]);
629
break;
630
case "describe": {
631
const message = await vscode.window.showInputBox({ prompt: "New message", value: info.message });
632
if (!message?.trim()) {
633
return;
634
}
635
await repo.oxy.describe(repo.root, `#${hash.slice(0, 12)}`, message.trim());
636
break;
637
}
638
case "tag": {
639
const name = await vscode.window.showInputBox({ prompt: "Tag name" });
640
if (!name?.trim()) {
641
return;
642
}
643
await repo.oxy.tagCreate(repo.root, name.trim(), [`#${hash.slice(0, 12)}`]);
644
break;
645
}
646
case "abandon": {
647
const confirm = await vscode.window.showWarningMessage(`Abandon "${subject}"?`, { modal: true }, "Abandon");
648
if (confirm !== "Abandon") {
649
return;
650
}
651
const out = await repo.oxy.abandon(repo.root, `#${hash.slice(0, 12)}`);
652
vscode.window.setStatusBarMessage(`$(trash) ${out.split("\n")[0]}`, 5000);
653
break;
654
}
655
case "copy":
656
await vscode.env.clipboard.writeText(info.change_id);
657
return;
658
}
659
await repo.refresh();
660
} catch (e) {
661
if (e instanceof OxyError && e.message.includes("has changes")) {
662
const confirm = await vscode.window.showWarningMessage("The working copy has uncommitted changes. Discard them and check out?", { modal: true }, "Discard and check out");
663
if (confirm) {
664
try {
665
await repo.oxy.checkout(repo.root, [`#${hash.slice(0, 12)}`], true);
666
await repo.refresh();
667
} catch (e2) {
668
showError(e2);
669
}
670
}
671
return;
672
}
673
showError(e);
674
}
675
}
676
677
async function fileHistory(uri: vscode.Uri | undefined): Promise<void> {
678
if (!uri) {
679
return;
680
}
681
const repo = [...repositories.values()].find((r) => r.relativePath(uri) !== undefined);
682
const rel = repo?.relativePath(uri);
683
if (!repo || !rel) {
684
void vscode.window.showInformationMessage("Oxygen: this file is not in an Oxygen repository.");
685
return;
686
}
687
try {
688
const log = await repo.oxy.log(repo.root, 200, rel);
689
const changes = log.changes.filter((c) => !c.working_copy);
690
if (changes.length === 0) {
691
void vscode.window.showInformationMessage(`Oxygen: no committed change touches ${rel}.`);
692
return;
693
}
694
const pick = await vscode.window.showQuickPick(
695
changes.map((c, i) => ({
696
label: `${c.head ? "$(circle-filled)" : "$(circle-outline)"} ${c.message.split("\n")[0]}`,
697
description: `${c.change_id.slice(0, 8)} · ${c.author} · ${timeAgo(c.when_ms)}`,
698
change: c,
699
previous: changes[i + 1],
700
})),
701
{ placeHolder: `${changes.length} change${changes.length === 1 ? "" : "s"} touched ${rel}; pick one to compare with the version before it`, matchOnDescription: true },
702
);
703
if (!pick) {
704
return;
705
}
706
const after = oxyUriFor(repo.root, rel, pick.change.hash);
707
if (pick.previous) {
708
const before = oxyUriFor(repo.root, rel, pick.previous.hash);
709
await vscode.commands.executeCommand("vscode.diff", before, after, `${basename(rel)} (${pick.previous.change_id.slice(0, 8)} ↔ ${pick.change.change_id.slice(0, 8)})`);
710
} else {
711
await vscode.commands.executeCommand("vscode.open", after);
712
}
713
} catch (e) {
714
showError(e);
715
}
716
}
717
718
function oxyUriFor(root: string, rel: string, hash: string): vscode.Uri {
719
return oxyUri(root, rel, `#${hash.slice(0, 12)}`);
720
}
721
c8f95589 Andy McCall 1 month ago ↶
722
// ---------------------------------------------------------------- bookmarks
723
724
async function switchBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
725
const repo = repositoryFor(arg);
726
if (!repo) {
727
return;
728
}
729
let bookmarks;
730
try {
731
bookmarks = await repo.oxy.bookmarks(repo.root);
732
} catch (e) {
733
showError(e);
734
return;
735
}
736
type Item = vscode.QuickPickItem & { action: "switch" | "create" | "merge" | "delete"; name?: string };
737
const items: Item[] = bookmarks.map((b) => ({
738
label: `${b.current ? "$(circle-filled)" : "$(bookmark)"} ${b.name}`,
739
description: `${b.heads.length} head${b.heads.length === 1 ? "" : "s"}${b.ahead ? ` $(arrow-up)${b.ahead}` : ""}${b.behind ? ` $(arrow-down)${b.behind}` : ""}`,
740
detail: b.current ? "current bookmark" : undefined,
741
action: "switch",
742
name: b.name,
743
}));
744
items.push(
745
{ label: "$(add) Create bookmark…", action: "create", alwaysShow: true },
746
{ label: "$(git-merge) Merge a bookmark into the current one…", action: "merge", alwaysShow: true },
747
{ label: "$(trash) Delete a bookmark…", action: "delete", alwaysShow: true },
748
);
749
const pick = await vscode.window.showQuickPick(items, { placeHolder: "Switch to a bookmark" });
750
if (!pick) {
751
return;
752
}
753
switch (pick.action) {
754
case "create":
755
return createBookmark(repo);
756
case "merge":
757
return mergeBookmark(repo);
758
case "delete":
759
return deleteBookmark(repo);
760
case "switch":
761
if (pick.name && !bookmarks.find((b) => b.name === pick.name)?.current) {
762
await doSwitch(repo, pick.name);
763
}
764
}
765
}
766
767
async function doSwitch(repo: Repository, name: string): Promise<void> {
768
try {
769
await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Switching to ${name}` }, () => repo.oxy.bookmarkSwitch(repo.root, name));
770
} catch (e) {
771
if (e instanceof OxyError && e.message.includes("has changes")) {
772
const pick = await vscode.window.showWarningMessage(`The working copy has uncommitted changes. Switching to ${name} would discard them.`, { modal: true }, "Discard and switch");
773
if (pick === "Discard and switch") {
774
try {
775
await repo.oxy.bookmarkSwitch(repo.root, name, true);
776
} catch (e2) {
777
showError(e2);
778
}
779
}
780
} else {
781
showError(e);
782
}
783
}
784
await repo.refresh();
785
}
786
787
async function createBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
788
const repo = repositoryFor(arg);
789
if (!repo) {
790
return;
791
}
792
const name = await vscode.window.showInputBox({
793
prompt: "New bookmark name (created at the working copy's base)",
794
validateInput: (v) => (!v.trim() || v.startsWith("-") || v.includes("..") || v.includes("//") || /[\s@{]/.test(v) ? "Not a valid bookmark name" : undefined),
795
});
796
if (!name) {
797
return;
798
}
799
try {
800
await repo.oxy.bookmarkCreate(repo.root, name.trim());
801
const pick = await vscode.window.showInformationMessage(`Oxygen: created bookmark ${name}.`, "Switch to it");
802
if (pick) {
803
await doSwitch(repo, name.trim());
804
}
805
await repo.refresh();
806
} catch (e) {
807
showError(e);
808
}
809
}
810
811
async function pickBookmark(repo: Repository, placeHolder: string, excludeCurrent: boolean): Promise<string | undefined> {
812
const bookmarks = await repo.oxy.bookmarks(repo.root);
813
const items = bookmarks.filter((b) => !excludeCurrent || !b.current).map((b) => ({ label: b.name, description: `${b.heads.length} head${b.heads.length === 1 ? "" : "s"}` }));
814
if (items.length === 0) {
815
void vscode.window.showInformationMessage("Oxygen: no other bookmarks.");
816
return undefined;
817
}
818
return (await vscode.window.showQuickPick(items, { placeHolder }))?.label;
819
}
820
821
async function mergeBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
822
const repo = repositoryFor(arg);
823
if (!repo) {
824
return;
825
}
826
try {
827
const name = await pickBookmark(repo, `Merge into ${repo.currentBookmark?.name ?? "the current bookmark"}`, true);
828
if (!name) {
829
return;
830
}
831
const out = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Merging ${name}` }, () => repo.oxy.merge(repo.root, name));
832
await repo.refresh();
833
if (repo.status?.conflicted) {
834
void vscode.window.showWarningMessage(`Oxygen: merged ${name}; the result has conflicts to resolve.`);
835
} else {
836
vscode.window.setStatusBarMessage(`$(git-merge) ${out.split("\n")[0]}`, 5000);
837
}
838
} catch (e) {
839
showError(e);
840
}
841
}
842
843
async function deleteBookmark(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
844
const repo = repositoryFor(arg);
845
if (!repo) {
846
return;
847
}
848
try {
849
const name = await pickBookmark(repo, "Delete which bookmark? Its changes stay.", true);
850
if (!name) {
851
return;
852
}
853
const pick = await vscode.window.showWarningMessage(`Delete bookmark ${name}?`, { modal: true }, "Delete");
854
if (pick !== "Delete") {
855
return;
856
}
857
await repo.oxy.bookmarkDelete(repo.root, name);
858
await repo.refresh();
859
} catch (e) {
860
showError(e);
861
}
862
}
863
864
// ---------------------------------------------------------------- sync
865
866
async function pickRemote(repo: Repository): Promise<string | undefined> {
867
const remotes = await repo.oxy.remotes(repo.root);
868
if (remotes.length === 0) {
869
void vscode.window.showErrorMessage("Oxygen: this repository has no remotes. Add one with oxy remote add <name> <url>.");
870
return undefined;
871
}
872
if (remotes.length === 1) {
873
return remotes[0].name;
874
}
875
return (await vscode.window.showQuickPick(remotes.map((r) => ({ label: r.name, description: r.url })), { placeHolder: "Remote" }))?.label;
876
}
877
878
async function fetch(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
879
const repo = repositoryFor(arg);
880
if (!repo) {
881
return;
882
}
883
try {
884
const remote = await pickRemote(repo);
885
if (!remote) {
886
return;
887
}
888
const info = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Fetching from ${remote}` }, () => repo.oxy.fetch(repo.root, remote));
889
await repo.refresh();
890
const fresh = info.refs.filter((r) => r.new > 0);
891
const summary = fresh.length ? fresh.map((r) => `${r.name}: ${r.new} new`).join(", ") : "nothing new";
892
vscode.window.setStatusBarMessage(`$(cloud-download) ${remote}: ${summary}`, 5000);
893
if (info.unverified) {
894
void vscode.window.showWarningMessage(`Oxygen: ${remote} has no trust policy; nothing fetched from it was verified.`);
895
}
896
} catch (e) {
897
showError(e);
898
}
899
}
900
901
async function pull(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
902
const repo = repositoryFor(arg);
903
if (!repo) {
904
return;
905
}
906
try {
907
const remote = await pickRemote(repo);
908
if (!remote) {
909
return;
910
}
911
const info = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Pulling from ${remote}` }, () => repo.oxy.pull(repo.root, remote));
912
await repo.refresh();
913
if (!info.changed) {
914
vscode.window.setStatusBarMessage("$(check) Already up to date", 4000);
915
} else if (info.conflicted) {
916
void vscode.window.showWarningMessage("Oxygen: pulled; the result has conflicts to resolve.");
917
} else if (!info.checked_out) {
918
void vscode.window.showInformationMessage("Oxygen: pulled; the working copy was not updated because it has changes. Check out when ready.");
919
} else {
920
vscode.window.setStatusBarMessage(`$(cloud-download) Pulled from ${remote}`, 4000);
921
}
922
} catch (e) {
923
showError(e);
924
}
925
}
926
927
async function push(arg: Repository | vscode.SourceControl | undefined, chooseBookmark: boolean): Promise<void> {
928
const repo = repositoryFor(arg);
929
if (!repo) {
930
return;
931
}
932
try {
933
const remote = await pickRemote(repo);
934
if (!remote) {
935
return;
936
}
937
let bookmark: string | undefined;
938
if (chooseBookmark) {
939
bookmark = await pickBookmark(repo, "Push which bookmark?", false);
940
if (!bookmark) {
941
return;
942
}
943
}
944
if (vscode.workspace.getConfiguration("oxygen").get<boolean>("confirmSync", true)) {
945
const pick = await vscode.window.showInformationMessage(`Push ${bookmark ?? repo.currentBookmark?.name ?? "the current bookmark"} to ${remote}?`, { modal: false }, "Push", "Always push without asking");
946
if (!pick) {
947
return;
948
}
949
if (pick === "Always push without asking") {
950
await vscode.workspace.getConfiguration("oxygen").update("confirmSync", false, vscode.ConfigurationTarget.Global);
951
}
952
}
953
const results = await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: `Pushing to ${remote}` }, () => repo.oxy.push(repo.root, remote, bookmark));
954
await repo.refresh();
955
const rejected = results.filter((r) => !r.ok);
956
if (rejected.length === 0) {
957
vscode.window.setStatusBarMessage(`$(cloud-upload) Pushed to ${remote}`, 4000);
958
return;
959
}
960
const reason = rejected.map((r) => `${r.name}: ${r.reason}`).join("; ");
961
const pick = await vscode.window.showErrorMessage(`Oxygen: push rejected. ${reason}`, "Pull", "Show Output");
962
if (pick === "Pull") {
963
await pull(repo);
964
} else if (pick === "Show Output") {
965
output.show();
966
}
967
} catch (e) {
968
if (e instanceof OxyError && e.message.includes("pull first")) {
969
const pick = await vscode.window.showWarningMessage(`Oxygen: ${e.message}`, "Pull");
970
if (pick === "Pull") {
971
await pull(repo);
972
}
973
return;
974
}
975
showError(e);
976
}
977
}
978
979
async function sync(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
980
const repo = repositoryFor(arg);
981
if (!repo) {
982
return;
983
}
984
await pull(repo);
985
if (repo.status?.conflicted) {
986
return;
987
}
988
if ((repo.currentBookmark?.ahead ?? 0) > 0) {
989
await push(repo, false);
990
}
991
}
992
35d56b58 Andy McCall 1 month ago ↶
993
/** The commit message from the input box, else a prompt. */
994
async function askMessage(repo: Repository, prompt: string): Promise<string | undefined> {
995
const fromBox = repo.sourceControl.inputBox.value.trim();
996
if (fromBox) {
997
return fromBox;
998
}
999
return (await vscode.window.showInputBox({ prompt, placeHolder: "Commit message" }))?.trim() || undefined;
1000
}
1001
bed495a9 Andy McCall 1 month ago ↶
1002
async function showLog(): Promise<void> {
1003
const repo = repositoryFor(undefined);
1004
if (!repo) {
1005
return;
1006
}
1007
try {
1008
const log = await repo.oxy.log(repo.root, 50);
1009
const items = log.changes
1010
.filter((c) => !c.working_copy)
1011
.map((c) => ({
1012
label: `${c.head ? "$(circle-filled)" : "$(circle-outline)"} ${c.message.split("\n")[0]}`,
1013
description: `${c.change_id.slice(0, 8)} · ${c.author}`,
1014
detail: `${new Date(c.when_ms).toLocaleString()} · ${c.signature?.status ?? "unsigned"}${c.signature?.revoked ? " (key revoked)" : ""}`,
1015
change: c,
1016
}));
1017
const pick = await vscode.window.showQuickPick(items, { placeHolder: `${log.bookmark}: ${log.total - 1} changes`, matchOnDescription: true, matchOnDetail: true });
1018
if (pick) {
1019
const doc = await vscode.workspace.openTextDocument({ content: pick.change.message, language: "markdown" });
1020
await vscode.window.showTextDocument(doc, { preview: true });
1021
}
1022
} catch (e) {
1023
showError(e);
1024
}
1025
}
1026
1027
function showError(e: unknown): void {
1028
const message = e instanceof OxyError ? e.message : String(e);
1029
void vscode.window.showErrorMessage(`Oxygen: ${message}`, "Show Output").then((pick) => pick && output.show());
1030
}