OXYGENHUB
# 691d366b4c69 oxygen-vscode / src / conflicts.ts
4.2 KiB · last changed Milestone 0.5: conflict editor CodeLens over oxy markers, keep-this-candidate for tree conflicts, mark resolved, commit resolution label 1 month ago source blame history raw
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
}