// The conflict editor (spec ยง5.7): CodeLens actions over each conflict // region, keep-this-candidate for tree conflicts, and mark-resolved. import * as vscode from "vscode"; import { basename, dirname, join } from "node:path"; import { promises as fs } from "node:fs"; import { ConflictRegion, candidateBase, parseConflicts, resolveRegion } from "./markers"; import { Repository } from "./repository"; export class ConflictCodeLensProvider implements vscode.CodeLensProvider { private readonly emitter = new vscode.EventEmitter(); readonly onDidChangeCodeLenses = this.emitter.event; constructor(private readonly repositories: () => Repository[]) {} refresh(): void { this.emitter.fire(); } provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { if (document.uri.scheme !== "file" || !this.repositories().some((r) => r.relativePath(document.uri) !== undefined)) { return []; } const text = document.getText(); if (!text.includes("<<<<<<< oxy conflict")) { return []; } const regions = parseConflicts(text.split(/\r?\n/)); const lenses: vscode.CodeLens[] = []; regions.forEach((region, index) => { const range = new vscode.Range(region.start, 0, region.start, 0); for (const side of region.sides) { const title = side.deleted ? `Accept deletion (${side.label.replace("deleted by change ", "")})` : `Accept side ${side.number} (${side.label})`; lenses.push(new vscode.CodeLens(range, { title, command: "oxygen.acceptSides", arguments: [document.uri, index, [side.number]] })); } if (region.sides.length > 1 && !region.sides.every((s) => s.deleted)) { lenses.push(new vscode.CodeLens(range, { title: "Accept all sides", command: "oxygen.acceptSides", arguments: [document.uri, index, region.sides.map((s) => s.number)] })); } lenses.push(new vscode.CodeLens(range, { title: `Conflict ${region.number} of ${region.total}`, command: "" })); }); return lenses; } } /** Replace conflict region `index` of the document with the kept sides. */ export async function acceptSides(uri: vscode.Uri, index: number, keep: number[]): Promise { const document = await vscode.workspace.openTextDocument(uri); const lines = document.getText().split(/\r?\n/); const regions = parseConflicts(lines); const region: ConflictRegion | undefined = regions[index]; if (!region) { return; } const replacement = resolveRegion(lines, region, keep); const edit = new vscode.WorkspaceEdit(); const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n"; const range = new vscode.Range(region.start, 0, region.end + 1, 0); edit.replace(uri, range, replacement.length ? replacement.join(eol) + eol : ""); await vscode.workspace.applyEdit(edit); await document.save(); } /** Keep one `.oxy-conflict-N` candidate under the real name; the others go. */ export async function keepCandidate(repo: Repository, candidatePath: string): Promise { const base = candidateBase(candidatePath); if (!base) { return; } const dir = join(repo.root, dirname(candidatePath)); const name = basename(base); const entries = await fs.readdir(dir); for (const e of entries) { if (e !== basename(candidatePath) && candidateBase(e) === name) { await fs.rm(join(dir, e), { recursive: true, force: true }); } } await fs.rename(join(repo.root, candidatePath), join(repo.root, base)); await repo.refresh(); } export async function markResolved(repo: Repository): Promise { const r = await repo.oxy.run(repo.root, ["resolve", "--json"]); await repo.refresh(); const status = repo.status; if (!status || !status.conflicted) { void vscode.window.showInformationMessage("Oxygen: no conflicts remain."); return; } const unresolved = status.conflicts.filter((c) => c.resolved !== true); if (unresolved.length === 0) { void vscode.window.showInformationMessage("Oxygen: every conflict is resolved; commit to record the resolution."); } else { const names = unresolved.map((c) => `${c.path ?? c.kind}${c.resolved === "markers" ? " (markers remain)" : ""}`).join(", "); void vscode.window.showWarningMessage(`Oxygen: still to resolve: ${names}`); } void r; }