| 25103ddd Andy McCall ↶ |
1 |
// The conflict editor (spec §5.7): CodeLens actions over each conflict |
| 2 |
// region, keep-this-candidate for tree conflicts, and mark-resolved. |
| 3 |
|
| 4 |
import * as vscode from "vscode"; |
| 5 |
import { basename, dirname, join } from "node:path"; |
| 6 |
import { promises as fs } from "node:fs"; |
| 7 |
import { ConflictRegion, candidateBase, parseConflicts, resolveRegion } from "./markers"; |
| 8 |
import { Repository } from "./repository"; |
| 9 |
|
| 10 |
export class ConflictCodeLensProvider implements vscode.CodeLensProvider { |
| 11 |
private readonly emitter = new vscode.EventEmitter<void>(); |
| 12 |
readonly onDidChangeCodeLenses = this.emitter.event; |
| 13 |
|
| 14 |
constructor(private readonly repositories: () => Repository[]) {} |
| 15 |
|
| 16 |
refresh(): void { |
| 17 |
this.emitter.fire(); |
| 18 |
} |
| 19 |
|
| 20 |
provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { |
| 21 |
if (document.uri.scheme !== "file" || !this.repositories().some((r) => r.relativePath(document.uri) !== undefined)) { |
| 22 |
return []; |
| 23 |
} |
| 24 |
const text = document.getText(); |
| 25 |
if (!text.includes("<<<<<<< oxy conflict")) { |
| 26 |
return []; |
| 27 |
} |
| 28 |
const regions = parseConflicts(text.split(/\r?\n/)); |
| 29 |
const lenses: vscode.CodeLens[] = []; |
| 30 |
regions.forEach((region, index) => { |
| 31 |
const range = new vscode.Range(region.start, 0, region.start, 0); |
| 32 |
for (const side of region.sides) { |
| 33 |
const title = side.deleted ? `Accept deletion (${side.label.replace("deleted by change ", "")})` : `Accept side ${side.number} (${side.label})`; |
| 34 |
lenses.push(new vscode.CodeLens(range, { title, command: "oxygen.acceptSides", arguments: [document.uri, index, [side.number]] })); |
| 35 |
} |
| 36 |
if (region.sides.length > 1 && !region.sides.every((s) => s.deleted)) { |
| 37 |
lenses.push(new vscode.CodeLens(range, { title: "Accept all sides", command: "oxygen.acceptSides", arguments: [document.uri, index, region.sides.map((s) => s.number)] })); |
| 38 |
} |
| 39 |
lenses.push(new vscode.CodeLens(range, { title: `Conflict ${region.number} of ${region.total}`, command: "" })); |
| 40 |
}); |
| 41 |
return lenses; |
| 42 |
} |
| 43 |
} |
| 44 |
|
| 45 |
/** Replace conflict region `index` of the document with the kept sides. */ |
| 46 |
export async function acceptSides(uri: vscode.Uri, index: number, keep: number[]): Promise<void> { |
| 47 |
const document = await vscode.workspace.openTextDocument(uri); |
| 48 |
const lines = document.getText().split(/\r?\n/); |
| 49 |
const regions = parseConflicts(lines); |
| 50 |
const region: ConflictRegion | undefined = regions[index]; |
| 51 |
if (!region) { |
| 52 |
return; |
| 53 |
} |
| 54 |
const replacement = resolveRegion(lines, region, keep); |
| 55 |
const edit = new vscode.WorkspaceEdit(); |
| 56 |
const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n"; |
| 57 |
const range = new vscode.Range(region.start, 0, region.end + 1, 0); |
| 58 |
edit.replace(uri, range, replacement.length ? replacement.join(eol) + eol : ""); |
| 59 |
await vscode.workspace.applyEdit(edit); |
| 60 |
await document.save(); |
| 61 |
} |
| 62 |
|
| 63 |
/** Keep one `.oxy-conflict-N` candidate under the real name; the others go. */ |
| 64 |
export async function keepCandidate(repo: Repository, candidatePath: string): Promise<void> { |
| 65 |
const base = candidateBase(candidatePath); |
| 66 |
if (!base) { |
| 67 |
return; |
| 68 |
} |
| 69 |
const dir = join(repo.root, dirname(candidatePath)); |
| 70 |
const name = basename(base); |
| 71 |
const entries = await fs.readdir(dir); |
| 72 |
for (const e of entries) { |
| 73 |
if (e !== basename(candidatePath) && candidateBase(e) === name) { |
| 74 |
await fs.rm(join(dir, e), { recursive: true, force: true }); |
| 75 |
} |
| 76 |
} |
| 77 |
await fs.rename(join(repo.root, candidatePath), join(repo.root, base)); |
| 78 |
await repo.refresh(); |
| 79 |
} |
| 80 |
|
| 81 |
export async function markResolved(repo: Repository): Promise<void> { |
| 82 |
const r = await repo.oxy.run(repo.root, ["resolve", "--json"]); |
| 83 |
await repo.refresh(); |
| 84 |
const status = repo.status; |
| 85 |
if (!status || !status.conflicted) { |
| 86 |
void vscode.window.showInformationMessage("Oxygen: no conflicts remain."); |
| 87 |
return; |
| 88 |
} |
| 89 |
const unresolved = status.conflicts.filter((c) => c.resolved !== true); |
| 90 |
if (unresolved.length === 0) { |
| 91 |
void vscode.window.showInformationMessage("Oxygen: every conflict is resolved; commit to record the resolution."); |
| 92 |
} else { |
| 93 |
const names = unresolved.map((c) => `${c.path ?? c.kind}${c.resolved === "markers" ? " (markers remain)" : ""}`).join(", "); |
| 94 |
void vscode.window.showWarningMessage(`Oxygen: still to resolve: ${names}`); |
| 95 |
} |
| 96 |
void r; |
| 97 |
} |