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";
7
import { Repository, OxyResourceState } from "./repository";
8
import { OxyContentProvider, SCHEME, baseUri } from "./contentProvider";
9
import { OxyDecorationProvider } from "./decorations";
10
11
const MIN_VERSION = { format: 2, protocol: 1 };
12
13
let output: vscode.OutputChannel;
14
let oxy: Oxy;
15
let contentProvider: OxyContentProvider;
16
let decorations: OxyDecorationProvider;
17
const repositories = new Map<string, Repository>();
18
19
export async function activate(context: vscode.ExtensionContext): Promise<void> {
20
output = vscode.window.createOutputChannel("Oxygen");
21
context.subscriptions.push(output);
810a1c53 Andy McCall 1 month ago ↶
22
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 ↶
23
oxy = new Oxy(findBinary(vscode.workspace.getConfiguration("oxygen").get<string>("path")), (line) => output.appendLine(line));
24
25
try {
26
const v = await oxy.version();
27
output.appendLine(`oxy ${v.version} (format ${v.format}, protocol ${v.protocol}) at ${oxy.binary}`);
28
if (v.format < MIN_VERSION.format || v.protocol < MIN_VERSION.protocol) {
29
void vscode.window.showWarningMessage(`Oxygen: oxy ${v.version} is older than this extension supports.`);
30
}
31
} catch (e) {
32
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");
33
if (pick === "Open Settings") {
34
void vscode.commands.executeCommand("workbench.action.openSettings", "oxygen.path");
35
}
36
return;
37
}
38
39
contentProvider = new OxyContentProvider(oxy);
40
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(SCHEME, contentProvider));
41
decorations = new OxyDecorationProvider(() => [...repositories.values()]);
42
context.subscriptions.push(decorations);
43
44
context.subscriptions.push(
45
vscode.commands.registerCommand("oxygen.refresh", async () => {
46
await Promise.all([...repositories.values()].map((r) => r.refresh()));
47
}),
48
vscode.commands.registerCommand("oxygen.showOutput", () => output.show()),
49
vscode.commands.registerCommand("oxygen.openFile", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)),
50
vscode.commands.registerCommand("oxygen.openChanges", (state?: OxyResourceState) => state && openChanges(state)),
51
vscode.commands.registerCommand("oxygen.openBaseVersion", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", baseUri(state.repository.root, state.path))),
52
vscode.commands.registerCommand("oxygen.commit", (arg?: Repository | vscode.SourceControl) => commit(arg)),
53
vscode.commands.registerCommand("oxygen.showLog", () => showLog()),
810a1c53 Andy McCall 1 month ago ↶
54
vscode.commands.registerCommand("oxygen.init", () => initRepository()),
bed495a9 Andy McCall 1 month ago ↶
55
vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()),
56
vscode.workspace.onDidChangeConfiguration((e) => {
57
if (e.affectsConfiguration("oxygen.path")) {
58
void vscode.window.showInformationMessage("Oxygen: reload the window to use the new oxy path.");
59
}
60
if (e.affectsConfiguration("oxygen")) {
61
decorations.refresh();
62
for (const r of repositories.values()) {
63
r.scheduleRefresh();
64
}
65
}
66
}),
67
);
68
context.subscriptions.push({ dispose: () => repositories.forEach((r) => r.dispose()) });
69
70
await discover();
71
}
72
73
export function deactivate(): void {
74
for (const r of repositories.values()) {
75
r.dispose();
76
}
77
repositories.clear();
78
}
79
80
/** One Repository per distinct oxy root among the workspace folders. */
81
async function discover(): Promise<void> {
82
const wanted = new Map<string, vscode.WorkspaceFolder>();
83
for (const folder of vscode.workspace.workspaceFolders ?? []) {
84
const info = await oxy.root(folder.uri.fsPath);
85
if (info) {
86
wanted.set(info.root, folder);
87
}
88
}
89
for (const [root, repo] of repositories) {
90
if (!wanted.has(root)) {
91
repo.dispose();
92
repositories.delete(root);
93
}
94
}
810a1c53 Andy McCall 1 month ago ↶
95
if (wanted.size === 0 && (vscode.workspace.workspaceFolders ?? []).length > 0) {
96
output.appendLine("no Oxygen repository in the open folders (oxy root found none)");
97
}
bed495a9 Andy McCall 1 month ago ↶
98
for (const [root, folder] of wanted) {
99
if (!repositories.has(root)) {
100
const repo = new Repository(oxy, root, folder);
101
repo.onDidChangeStatus(() => {
102
decorations.refresh();
103
contentProvider.invalidate(root);
104
});
105
repositories.set(root, repo);
106
output.appendLine(`repository ${root}`);
107
await repo.refresh();
108
}
109
}
110
void vscode.commands.executeCommand("setContext", "oxygen.hasRepository", repositories.size > 0);
111
}
112
113
function repositoryFor(arg: Repository | vscode.SourceControl | undefined): Repository | undefined {
114
if (arg instanceof Repository) {
115
return arg;
116
}
117
if (arg && "rootUri" in arg && arg.rootUri) {
118
return repositories.get(arg.rootUri.fsPath);
119
}
120
const active = vscode.window.activeTextEditor?.document.uri;
121
for (const repo of repositories.values()) {
122
if (active && repo.relativePath(active) !== undefined) {
123
return repo;
124
}
125
}
126
return repositories.size === 1 ? [...repositories.values()][0] : undefined;
127
}
128
129
async function openChanges(state: OxyResourceState): Promise<void> {
130
const repo = state.repository;
131
const title = `${basename(state.path)} (base ↔ working copy)`;
132
if (state.status === "Added") {
133
await vscode.commands.executeCommand("vscode.open", state.resourceUri);
134
return;
135
}
136
if (state.status === "Removed") {
137
await vscode.commands.executeCommand("vscode.open", baseUri(repo.root, state.path));
138
return;
139
}
140
await vscode.commands.executeCommand("vscode.diff", baseUri(repo.root, state.path), state.resourceUri, title);
141
}
142
143
async function commit(arg: Repository | vscode.SourceControl | undefined): Promise<void> {
144
const repo = repositoryFor(arg);
145
if (!repo) {
146
void vscode.window.showErrorMessage("Oxygen: no repository for the active file.");
147
return;
148
}
149
let message = repo.sourceControl.inputBox.value.trim();
150
if (!message) {
151
message = (await vscode.window.showInputBox({ prompt: "Commit message", placeHolder: "What did you change?" }))?.trim() ?? "";
152
}
153
if (!message) {
154
return;
155
}
156
try {
157
await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message));
158
repo.sourceControl.inputBox.value = "";
159
await repo.refresh();
160
} catch (e) {
161
showError(e);
162
}
163
}
164
810a1c53 Andy McCall 1 month ago ↶
165
async function initRepository(): Promise<void> {
166
const folders = vscode.workspace.workspaceFolders ?? [];
167
if (folders.length === 0) {
168
void vscode.window.showErrorMessage("Oxygen: open a folder first.");
169
return;
170
}
171
const folder = folders.length === 1 ? folders[0] : await vscode.window.showWorkspaceFolderPick({ placeHolder: "Folder to initialize as an Oxygen repository" });
172
if (!folder) {
173
return;
174
}
175
try {
176
await oxy.check(folder.uri.fsPath, ["init"]);
177
await discover();
178
void vscode.window.showInformationMessage(`Oxygen: initialized ${folder.name}.`);
179
} catch (e) {
180
showError(e);
181
}
182
}
183
bed495a9 Andy McCall 1 month ago ↶
184
async function showLog(): Promise<void> {
185
const repo = repositoryFor(undefined);
186
if (!repo) {
187
return;
188
}
189
try {
190
const log = await repo.oxy.log(repo.root, 50);
191
const items = log.changes
192
.filter((c) => !c.working_copy)
193
.map((c) => ({
194
label: `${c.head ? "$(circle-filled)" : "$(circle-outline)"} ${c.message.split("\n")[0]}`,
195
description: `${c.change_id.slice(0, 8)} · ${c.author}`,
196
detail: `${new Date(c.when_ms).toLocaleString()} · ${c.signature?.status ?? "unsigned"}${c.signature?.revoked ? " (key revoked)" : ""}`,
197
change: c,
198
}));
199
const pick = await vscode.window.showQuickPick(items, { placeHolder: `${log.bookmark}: ${log.total - 1} changes`, matchOnDescription: true, matchOnDetail: true });
200
if (pick) {
201
const doc = await vscode.workspace.openTextDocument({ content: pick.change.message, language: "markdown" });
202
await vscode.window.showTextDocument(doc, { preview: true });
203
}
204
} catch (e) {
205
showError(e);
206
}
207
}
208
209
function showError(e: unknown): void {
210
const message = e instanceof OxyError ? e.message : String(e);
211
void vscode.window.showErrorMessage(`Oxygen: ${message}`, "Show Output").then((pick) => pick && output.show());
212
}