// Activation: find oxy, find repositories, wire the source control view, // decorations, the content provider, and the commands (spec §3). import * as vscode from "vscode"; import { basename } from "node:path"; import { Oxy, OxyError, findBinary } from "./oxy"; import { Repository, OxyResourceState } from "./repository"; import { OxyContentProvider, SCHEME, baseUri } from "./contentProvider"; import { OxyDecorationProvider } from "./decorations"; const MIN_VERSION = { format: 2, protocol: 1 }; let output: vscode.OutputChannel; let oxy: Oxy; let contentProvider: OxyContentProvider; let decorations: OxyDecorationProvider; const repositories = new Map(); export async function activate(context: vscode.ExtensionContext): Promise { output = vscode.window.createOutputChannel("Oxygen"); context.subscriptions.push(output); output.appendLine(`Oxygen extension ${context.extension.packageJSON.version} activating; folders: ${(vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath).join(", ") || "(none)"}`); oxy = new Oxy(findBinary(vscode.workspace.getConfiguration("oxygen").get("path")), (line) => output.appendLine(line)); try { const v = await oxy.version(); output.appendLine(`oxy ${v.version} (format ${v.format}, protocol ${v.protocol}) at ${oxy.binary}`); if (v.format < MIN_VERSION.format || v.protocol < MIN_VERSION.protocol) { void vscode.window.showWarningMessage(`Oxygen: oxy ${v.version} is older than this extension supports.`); } } catch (e) { 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"); if (pick === "Open Settings") { void vscode.commands.executeCommand("workbench.action.openSettings", "oxygen.path"); } return; } contentProvider = new OxyContentProvider(oxy); context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(SCHEME, contentProvider)); decorations = new OxyDecorationProvider(() => [...repositories.values()]); context.subscriptions.push(decorations); context.subscriptions.push( vscode.commands.registerCommand("oxygen.refresh", async () => { await Promise.all([...repositories.values()].map((r) => r.refresh())); }), vscode.commands.registerCommand("oxygen.showOutput", () => output.show()), vscode.commands.registerCommand("oxygen.openFile", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", state.resourceUri)), vscode.commands.registerCommand("oxygen.openChanges", (state?: OxyResourceState) => state && openChanges(state)), vscode.commands.registerCommand("oxygen.openBaseVersion", (state?: OxyResourceState) => state && vscode.commands.executeCommand("vscode.open", baseUri(state.repository.root, state.path))), vscode.commands.registerCommand("oxygen.commit", (arg?: Repository | vscode.SourceControl) => commit(arg)), vscode.commands.registerCommand("oxygen.showLog", () => showLog()), vscode.commands.registerCommand("oxygen.init", () => initRepository()), vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()), vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("oxygen.path")) { void vscode.window.showInformationMessage("Oxygen: reload the window to use the new oxy path."); } if (e.affectsConfiguration("oxygen")) { decorations.refresh(); for (const r of repositories.values()) { r.scheduleRefresh(); } } }), ); context.subscriptions.push({ dispose: () => repositories.forEach((r) => r.dispose()) }); await discover(); } export function deactivate(): void { for (const r of repositories.values()) { r.dispose(); } repositories.clear(); } /** One Repository per distinct oxy root among the workspace folders. */ async function discover(): Promise { const wanted = new Map(); for (const folder of vscode.workspace.workspaceFolders ?? []) { const info = await oxy.root(folder.uri.fsPath); if (info) { wanted.set(info.root, folder); } } for (const [root, repo] of repositories) { if (!wanted.has(root)) { repo.dispose(); repositories.delete(root); } } if (wanted.size === 0 && (vscode.workspace.workspaceFolders ?? []).length > 0) { output.appendLine("no Oxygen repository in the open folders (oxy root found none)"); } for (const [root, folder] of wanted) { if (!repositories.has(root)) { const repo = new Repository(oxy, root, folder); repo.onDidChangeStatus(() => { decorations.refresh(); contentProvider.invalidate(root); }); repositories.set(root, repo); output.appendLine(`repository ${root}`); await repo.refresh(); } } void vscode.commands.executeCommand("setContext", "oxygen.hasRepository", repositories.size > 0); } function repositoryFor(arg: Repository | vscode.SourceControl | undefined): Repository | undefined { if (arg instanceof Repository) { return arg; } if (arg && "rootUri" in arg && arg.rootUri) { return repositories.get(arg.rootUri.fsPath); } const active = vscode.window.activeTextEditor?.document.uri; for (const repo of repositories.values()) { if (active && repo.relativePath(active) !== undefined) { return repo; } } return repositories.size === 1 ? [...repositories.values()][0] : undefined; } async function openChanges(state: OxyResourceState): Promise { const repo = state.repository; const title = `${basename(state.path)} (base ↔ working copy)`; if (state.status === "Added") { await vscode.commands.executeCommand("vscode.open", state.resourceUri); return; } if (state.status === "Removed") { await vscode.commands.executeCommand("vscode.open", baseUri(repo.root, state.path)); return; } await vscode.commands.executeCommand("vscode.diff", baseUri(repo.root, state.path), state.resourceUri, title); } async function commit(arg: Repository | vscode.SourceControl | undefined): Promise { const repo = repositoryFor(arg); if (!repo) { void vscode.window.showErrorMessage("Oxygen: no repository for the active file."); return; } let message = repo.sourceControl.inputBox.value.trim(); if (!message) { message = (await vscode.window.showInputBox({ prompt: "Commit message", placeHolder: "What did you change?" }))?.trim() ?? ""; } if (!message) { return; } try { await vscode.window.withProgress({ location: vscode.ProgressLocation.SourceControl, title: "Committing" }, () => repo.oxy.commit(repo.root, message)); repo.sourceControl.inputBox.value = ""; await repo.refresh(); } catch (e) { showError(e); } } async function initRepository(): Promise { const folders = vscode.workspace.workspaceFolders ?? []; if (folders.length === 0) { void vscode.window.showErrorMessage("Oxygen: open a folder first."); return; } const folder = folders.length === 1 ? folders[0] : await vscode.window.showWorkspaceFolderPick({ placeHolder: "Folder to initialize as an Oxygen repository" }); if (!folder) { return; } try { await oxy.check(folder.uri.fsPath, ["init"]); await discover(); void vscode.window.showInformationMessage(`Oxygen: initialized ${folder.name}.`); } catch (e) { showError(e); } } async function showLog(): Promise { const repo = repositoryFor(undefined); if (!repo) { return; } try { const log = await repo.oxy.log(repo.root, 50); const items = log.changes .filter((c) => !c.working_copy) .map((c) => ({ label: `${c.head ? "$(circle-filled)" : "$(circle-outline)"} ${c.message.split("\n")[0]}`, description: `${c.change_id.slice(0, 8)} · ${c.author}`, detail: `${new Date(c.when_ms).toLocaleString()} · ${c.signature?.status ?? "unsigned"}${c.signature?.revoked ? " (key revoked)" : ""}`, change: c, })); const pick = await vscode.window.showQuickPick(items, { placeHolder: `${log.bookmark}: ${log.total - 1} changes`, matchOnDescription: true, matchOnDetail: true }); if (pick) { const doc = await vscode.workspace.openTextDocument({ content: pick.change.message, language: "markdown" }); await vscode.window.showTextDocument(doc, { preview: true }); } } catch (e) { showError(e); } } function showError(e: unknown): void { const message = e instanceof OxyError ? e.message : String(e); void vscode.window.showErrorMessage(`Oxygen: ${message}`, "Show Output").then((pick) => pick && output.show()); }