OXYGENHUB
◆ main oxygen-vscode / src / markers.ts
3.1 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
1
// Parser for oxy conflict markers (spec §9.5). No vscode imports, so it is
2
// tested with plain Node.
3
//
4
//   <<<<<<< oxy conflict 1 of 2, side 1 of 2: change k7m2p9 "Add retry loop"
5
//   ...
6
//   ======= oxy conflict 1 of 2, side 2 of 2: change q3x8w1 "Refactor client"
7
//   ...
8
//   >>>>>>> oxy conflict 1 of 2 end
9
//
10
// A delete/insert conflict's first side reads `deleted by change X` and is
11
// followed by a `-------` line and the deleted context.
12
13
export interface ConflictSide {
14
  /** 1-based side number. */
15
  number: number;
16
  /** `k7m2p9 "Add retry loop"` or `deleted by change k7m2p9`. */
17
  label: string;
18
  deleted: boolean;
19
  /** Line index of the side's header. */
20
  header: number;
21
  /** Content lines (exclusive end). */
22
  start: number;
23
  end: number;
24
}
25
26
export interface ConflictRegion {
27
  number: number;
28
  total: number;
29
  /** Line index of `<<<<<<<`. */
30
  start: number;
31
  /** Line index of `>>>>>>>`. */
32
  end: number;
33
  sides: ConflictSide[];
34
}
35
36
const OPEN = /^<<<<<<< oxy conflict (\d+) of (\d+), side (\d+) of (\d+): (.*)$/;
37
const NEXT = /^======= oxy conflict (\d+) of (\d+), side (\d+) of (\d+): (.*)$/;
38
const CLOSE = /^>>>>>>> oxy conflict (\d+) of (\d+) end$/;
39
40
export function parseConflicts(lines: string[]): ConflictRegion[] {
41
  const regions: ConflictRegion[] = [];
42
  let current: ConflictRegion | undefined;
43
  let side: ConflictSide | undefined;
44
  for (let i = 0; i < lines.length; i++) {
45
    const line = lines[i];
46
    const open = OPEN.exec(line);
47
    if (open && !current) {
48
      current = { number: Number(open[1]), total: Number(open[2]), start: i, end: -1, sides: [] };
49
      side = newSide(Number(open[3]), open[5], i);
50
      if (side.deleted && lines[i + 1] === "-------") {
51
        side.start = i + 2;
52
        i += 1;
53
      }
54
      current.sides.push(side);
55
      continue;
56
    }
57
    const next = current ? NEXT.exec(line) : null;
58
    if (next && current && side) {
59
      side.end = i;
60
      side = newSide(Number(next[3]), next[5], i);
61
      current.sides.push(side);
62
      continue;
63
    }
64
    const close = current ? CLOSE.exec(line) : null;
65
    if (close && current && side) {
66
      side.end = i;
67
      current.end = i;
68
      regions.push(current);
69
      current = undefined;
70
      side = undefined;
71
    }
72
  }
73
  return regions;
74
}
75
76
function newSide(number: number, label: string, header: number): ConflictSide {
77
  const deleted = label.startsWith("deleted by change ");
78
  return { number, label: deleted ? label : label.replace(/^change /, ""), deleted, header, start: header + 1, end: header + 1 };
79
}
80
81
/** The lines that replace a region when `keep` (side numbers) are kept, in order. */
82
export function resolveRegion(lines: string[], region: ConflictRegion, keep: number[]): string[] {
83
  const out: string[] = [];
84
  for (const n of keep) {
85
    const side = region.sides.find((s) => s.number === n);
86
    if (!side || side.deleted) {
87
      continue;
88
    }
89
    out.push(...lines.slice(side.start, side.end));
90
  }
91
  return out;
92
}
93
94
/** A conflict candidate file's real name, or undefined. */
95
export function candidateBase(path: string): string | undefined {
96
  const m = /^(.*)\.oxy-conflict-\d+$/.exec(path);
97
  return m ? m[1] : undefined;
98
}