OXYGENHUB

Coverage / src/markers.ts 88.8%

87 of 98 lines run, 5 of 5 functions, 30 of 30 branches · run #23 on main · run missed

10×
// Parser for oxy conflict markers (spec §9.5). No vscode imports, so it is
20×
// tested with plain Node.
30×
//
40×
//   <<<<<<< oxy conflict 1 of 2, side 1 of 2: change k7m2p9 "Add retry loop"
50×
//   ...
60×
//   ======= oxy conflict 1 of 2, side 2 of 2: change q3x8w1 "Refactor client"
70×
//   ...
80×
//   >>>>>>> oxy conflict 1 of 2 end
90×
//
100×
// A delete/insert conflict's first side reads `deleted by change X` and is
110×
// followed by a `-------` line and the deleted context.
121×
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
}
251×
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
}
351×
361×
const OPEN = /^<<<<<<< oxy conflict (\d+) of (\d+), side (\d+) of (\d+): (.*)$/;
371×
const NEXT = /^======= oxy conflict (\d+) of (\d+), side (\d+) of (\d+): (.*)$/;
381×
const CLOSE = /^>>>>>>> oxy conflict (\d+) of (\d+) end$/;
393×
403×
export function parseConflicts(lines: string[]): ConflictRegion[] {
413×
  const regions: ConflictRegion[] = [];
423×
  let current: ConflictRegion | undefined;
433×
  let side: ConflictSide | undefined;
443×
  for (let i = 0; i < lines.length; i++) {
4517×
    const line = lines[i];
4617×
    const open = OPEN.exec(line);
4717×
    if (open && !current) {
482×
      current = { number: Number(open[1]), total: Number(open[2]), start: i, end: -1, sides: [] };
492×
      side = newSide(Number(open[3]), open[5], i);
502×
      if (side.deleted && lines[i + 1] === "-------") {
511×
        side.start = i + 2;
521×
        i += 1;
531×
      }
542×
      current.sides.push(side);
552×
      continue;
562×
    }
5717×
    const next = current ? NEXT.exec(line) : null;
5817×
    if (next && current && side) {
593×
      side.end = i;
603×
      side = newSide(Number(next[3]), next[5], i);
613×
      current.sides.push(side);
623×
      continue;
633×
    }
6417×
    const close = current ? CLOSE.exec(line) : null;
6517×
    if (close && current && side) {
662×
      side.end = i;
672×
      current.end = i;
682×
      regions.push(current);
692×
      current = undefined;
702×
      side = undefined;
712×
    }
7217×
  }
733×
  return regions;
745×
}
755×
765×
function newSide(number: number, label: string, header: number): ConflictSide {
775×
  const deleted = label.startsWith("deleted by change ");
785×
  return { number, label: deleted ? label : label.replace(/^change /, ""), deleted, header, start: header + 1, end: header + 1 };
794×
}
804×
814×
/** The lines that replace a region when `keep` (side numbers) are kept, in order. */
824×
export function resolveRegion(lines: string[], region: ConflictRegion, keep: number[]): string[] {
834×
  const out: string[] = [];
844×
  for (const n of keep) {
855×
    const side = region.sides.find((s) => s.number === n);
865×
    if (!side || side.deleted) {
871×
      continue;
881×
    }
894×
    out.push(...lines.slice(side.start, side.end));
904×
  }
914×
  return out;
922×
}
932×
942×
/** A conflict candidate file's real name, or undefined. */
952×
export function candidateBase(path: string): string | undefined {
962×
  const m = /^(.*)\.oxy-conflict-\d+$/.exec(path);
972×
  return m ? m[1] : undefined;
982×
}