UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

467 lines • 18.5 kB
import { getOpcodes } from "../../../builtin/diff_format.js"; import { FUNCNAME_START, GIT_SPACE } from "./constants.js"; // Measured against git 2.47.3 and 2.50.1. Both print a -U0 hunk that reaches // past the last line with every count one short, so dropping a final line // both parents had reads "@@@ -3,0 -3,0 +3,18446744073709551615 @@@". This // prints the counts of the lines shown: "@@@ -3,1 -3,1 +3,0 @@@". const CONTEXT = 3; const COMMENT_BYTES = 40; // How many bits of comparison rows the folding of dropped lines keeps at // once, and how many it spends keeping the bit masks of repeated lines. const ROW_BUDGET = 2 ** 26; const MASK_BUDGET = 2 ** 26; const ENC = new TextEncoder(); const DEC = new TextDecoder(); /** * The hunks of a combined diff, one column per parent. * * Each parent is compared with the result on its own. A result line the * parent lacks shows `+` in its column, a line it had that the merge dropped * shows `-`, and lines several parents dropped at one row print once with * each of those parents marked. * * A hunk shows `context` lines around each changed row and takes in the next * changed row once their context lines meet or touch. Under `--cc` the * changed rows are first bunched more tightly (the next row falls within the * trailing context, or right after), and a bunch whose every line is new to, * or dropped by, the same parents, but not all of them, is taken as cleanly * merged and left out. Its dropped lines still count in the parents' ranges * where a shown hunk covers their row, as git counts them. * * @param parents - each parent's lines, newlines kept. * @param result - the merge result's lines, newlines kept. * @param dense - `--cc` rather than `-c`. * @param context - the requested number of context lines. */ export function combinedLines(parents, result, dense, context = CONTEXT) { const layout = compareSides(parents, result); let marks = changedRows(layout, context); if (dense) { const every = 2 ** layout.width - 1; marks = bunches(marks, 0, 0) .filter((bunch) => conflicted(bunch, every)) .flat(); } const before = linesBefore(layout); const out = []; let searched = 0; for (const hunk of bunches(marks, context, 1)) { const first = Math.max((hunk[0]?.row ?? 0) - context, 0); let reach = -Infinity; for (const mark of hunk) reach = Math.max(reach, mark.row, mark.reach); const last = Math.min(reach, result.length); let label = null; for (let row = first - 1; row >= searched; row--) { const line = result[row] ?? ''; if (FUNCNAME_START.test(line)) { label = line; break; } } // Row by row: a hunk can hold more rows than a call takes arguments. for (const line of renderHunk(layout, hunk, first, last, context, before, label)) out.push(line); searched = last + 1; } return out; } /** Compare every parent with the result. */ function compareSides(parents, result) { const newcomers = new Array(result.length).fill(0); const gaps = Array.from({ length: result.length + 1 }, () => []); parents.forEach((lines, index) => { const bit = 2 ** index; for (const [tag, lo, hi, start, stop] of getOpcodes(lines, result)) { if (tag === 'equal') continue; for (let row = start; row < stop; row++) newcomers[row] = (newcomers[row] ?? 0) | bit; if (hi > lo) gaps[start] = coalesce(gaps[start] ?? [], lines.slice(lo, hi), bit); } }); return { result, newcomers, gaps, width: parents.length }; } /** Every changed row, in order. */ function changedRows(layout, context) { const marks = []; layout.gaps.forEach((dropped, row) => { const fresh = row < layout.result.length ? (layout.newcomers[row] ?? 0) : 0; if (!fresh && dropped.length === 0) return; const kinds = new Set(dropped.map(([, bits]) => bits)); if (fresh) kinds.add(fresh); const reach = fresh ? row + context : row + context - 1; marks.push({ row, reach, kinds }); }); return marks; } /** * Split the changed rows into runs. A row joins the run before it when, * reaching back `lead` lines, it lands no later than right after the run's * last row, or `past` lines after the furthest trailing context in the run. */ function bunches(marks, lead, past) { const runs = []; let end = 0; for (const mark of marks) { const run = runs.at(-1); if (run && mark.row - lead <= end) { run.push(mark); } else { runs.push([mark]); end = 0; } end = Math.max(end, mark.reach + past, mark.row + 1); } return runs; } /** * Whether `--cc` shows a bunch: its lines differ from different sets of * parents, or some line differs from all of them. */ function conflicted(bunch, every) { const kinds = new Set(bunch.flatMap((mark) => [...mark.kinds])); return kinds.size > 1 || kinds.has(every); } /** Per row, how many lines of each parent come before it. */ function linesBefore(layout) { const seen = new Array(layout.width).fill(0); const table = []; layout.gaps.forEach((dropped, row) => { table.push([...seen]); for (const [, bits] of dropped) { for (let index = 0; index < layout.width; index++) seen[index] = (seen[index] ?? 0) + ((bits >> index) & 1); } if (row < layout.result.length) { const fresh = layout.newcomers[row] ?? 0; for (let index = 0; index < layout.width; index++) seen[index] = (seen[index] ?? 0) + 1 - ((fresh >> index) & 1); } }); return table; } /** * One hunk: its header, then rows `first` to `last`. A row's dropped lines * print when the row is one of the hunk's changes and always count. Its line * prints, and counts, when some change's context covers it; only under -U0 * can a row in the middle of a hunk go without (a row that only lost lines), * and git's header still starts at `first`. */ function renderHunk(layout, hunk, first, last, context, before, label) { const width = layout.width; const rows = new Set(hunk.map((mark) => mark.row)); const counts = new Array(width).fill(0); let shown = 0; const body = []; let pending = 0, covered = -1; for (let row = first; row <= last; row++) { for (const [line, bits] of layout.gaps[row] ?? []) { for (let index = 0; index < width; index++) counts[index] = (counts[index] ?? 0) + ((bits >> index) & 1); if (rows.has(row)) body.push(columns(bits, '-', width) + eol(line)); } while (pending < hunk.length && (hunk[pending]?.row ?? 0) - context <= row) { covered = Math.max(covered, hunk[pending]?.reach ?? 0); pending++; } if (row >= layout.result.length || row > covered) continue; const fresh = layout.newcomers[row] ?? 0; for (let index = 0; index < width; index++) counts[index] = (counts[index] ?? 0) + 1 - ((fresh >> index) & 1); shown++; body.push(columns(fresh, '+', width) + eol(layout.result[row] ?? '')); } const spans = counts .map((count, index) => `-${String((before[first]?.[index] ?? 0) + 1)},${String(count)}`) .join(' '); const fence = '@'.repeat(width + 1); const header = `${fence} ${spans} +${String(first + 1)},${String(shown)} ${fence}`; return [header + funcname(label) + '\n', ...body]; } /** The per-parent prefix: `sign` for each parent in `bits`. */ function columns(bits, sign, width) { let prefix = ''; for (let index = 0; index < width; index++) prefix += (bits >> index) & 1 ? sign : ' '; return prefix; } /** * Fold one more parent's dropped lines into a row. The lines pair with the * row's earlier ones along a longest common subsequence; between two pairs * the earlier parents' unpaired lines come first, then this parent's. */ function coalesce(held, dropped, bit) { if (held.length === 0) return dropped.map((line) => [line, bit]); const pairs = pairing(held.map(([line]) => bare(line)), dropped.map(bare)); const merged = []; let old = 0, fresh = 0; for (const [left, right] of [...pairs, [held.length, dropped.length]]) { for (let at = old; at < left; at++) { const removal = held[at]; if (removal) merged.push(removal); } for (const line of dropped.slice(fresh, right)) merged.push([line, bit]); const kept = held[left]; if (kept) merged.push([kept[0], kept[1] | bit]); old = left + 1; fresh = right + 1; } return merged; } /** * A line without its newline, so a dropped final line with none still pairs * with the same line elsewhere. */ function bare(line) { return line.endsWith('\n') ? line.slice(0, -1) : line; } /** * Index pairs of equal lines along a longest common subsequence, taken from * the ends inward: equal last lines pair, and where skipping a line on * either side keeps the longest length, the right side's line is skipped. * So `a b` and `b a` pair their `b`s, and `a a` and `a` pair the second `a`, * as git does. * * The walk needs the comparison row of every left line it passes, so rather * than keeping them all it keeps `budget` bits of rows and works the rest * out again from rows it kept: two passes over the lines while * `left.length * right.length ** 2` is under about `budget ** 2 / 4`, one * more per halving beyond that, with one extra row kept per halving. */ export function pairing(left, right, budget = ROW_BUDGET) { if (left.length === 0 || right.length === 0) return []; const spots = new Spots(right); const pairs = []; trace(left, spots, 0, left.length, spots.full(), right.length, pairs, budget); return pairs.reverse(); } /** * Where each line of `right` sits, as bit masks over `right` held in 32-bit * words. A line's mask is built from its positions when a row needs it; the * masks of lines that repeat are kept while `MASK_BUDGET` lasts. The rows a * stretch is walked through share one block, reused stretch after stretch. */ class Spots { right; width; words; last; places = new Map(); kept = new Map(); spare = MASK_BUDGET; scratch = null; block = null; constructor(right) { this.right = right; this.width = right.length; this.words = Math.max(Math.ceil(right.length / 32), 1); const unused = this.words * 32 - right.length; this.last = unused ? 0xffffffff >>> unused : 0xffffffff; right.forEach((line, j) => { const at = this.places.get(line); if (at) at.push(j); else this.places.set(line, [j]); }); } /** A fresh row with every bit of `right` set. */ full() { const row = new Uint32Array(this.words).fill(0xffffffff); row[this.words - 1] = this.last; return row; } /** `count` row views over the shared block, valid until the next call. */ rows(count) { if (!this.block || this.block.length < count * this.words) this.block = new Uint32Array(count * this.words); const block = this.block; return Array.from({ length: count }, (_, k) => block.subarray(k * this.words, (k + 1) * this.words)); } /** * Write into `into` the row after `row` once `line` is added to the left * side. Bit `j` of row `i` is clear when `right[j]` lengthens the longest * common subsequence of `left[:i]` and `right[:j + 1]`, so a row costs a * pass over its words rather than over `right`. */ step(row, line, into) { const spots = this.mask(line); if (!spots) { into.set(row); return; } let carry = 0; for (let w = 0; w < this.words; w++) { const bits = row[w] ?? 0; const hits = (bits & (spots[w] ?? 0)) >>> 0; const sum = bits + hits + carry; carry = sum > 0xffffffff ? 1 : 0; into[w] = ((sum >>> 0) | (bits & ~hits)) >>> 0; } into[this.words - 1] = ((into[this.words - 1] ?? 0) & this.last) >>> 0; if (spots === this.scratch) this.clear(line); } /** The bits of the positions where `line` sits in `right`, or null. */ mask(line) { const kept = this.kept.get(line); if (kept) return kept; const places = this.places.get(line); if (!places) return null; const repeated = places.length > 1 && this.spare >= this.width; if (repeated) this.spare -= this.width; const bits = repeated ? new Uint32Array(this.words) : (this.scratch ??= new Uint32Array(this.words)); for (const j of places) bits[j >>> 5] = ((bits[j >>> 5] ?? 0) | (1 << (j & 31))) >>> 0; if (repeated) this.kept.set(line, bits); return bits; } clear(line) { const scratch = this.scratch; if (scratch) for (const j of this.places.get(line) ?? []) scratch[j >>> 5] = 0; } } /** * Walk the pairing back from row `bottom` to row `top`, given row `top`'s * bits, and return the column it reaches row `top` at (0 once it has run out * of right lines). A stretch whose rows fit the budget is walked whole. A * longer one keeps every `stride`-th row from one pass and walks the pieces * from the last up, each worked out afresh from its first row; when even * those rows overflow half the budget, the stretch is halved first. */ function trace(left, spots, top, bottom, first, column, pairs, budget) { const span = bottom - top, width = Math.max(spots.width, 1); if (span <= 1 || (span + 1) * width <= budget) { const rows = spots.rows(span + 1); rows[0]?.set(first); for (let i = top; i < bottom; i++) { const from = rows[i - top], into = rows[i - top + 1]; if (from && into) spots.step(from, left[i] ?? '', into); } return walk(left, spots.right, rows, top, bottom, column, pairs); } const stride = Math.max(Math.floor(budget / (2 * width)), 1); if (Math.ceil(span / stride) * width > Math.floor(budget / 2)) { const middle = Math.floor((top + bottom) / 2); column = trace(left, spots, middle, bottom, advance(spots, left, top, middle, first), column, pairs, budget); if (!column) return 0; return trace(left, spots, top, middle, first, column, pairs, budget); } const starts = [first]; for (let start = top + stride; start < bottom; start += stride) starts.push(advance(spots, left, start - stride, start, starts.at(-1) ?? first)); while (starts.length > 0 && column) { const start = top + (starts.length - 1) * stride; const stop = Math.min(start + stride, bottom); column = trace(left, spots, start, stop, starts.pop() ?? first, column, pairs, budget); } return column; } /** Row `stop`'s bits, worked out from row `top`'s, in a fresh row. */ function advance(spots, left, top, stop, row) { let from = row.slice(), into = new Uint32Array(spots.words); for (let i = top; i < stop; i++) { spots.step(from, left[i] ?? '', into); [from, into] = [into, from]; } return from; } /** * Walk the pairing back through rows `top` to `row`, given those rows, and * return the column it reaches row `top` at (0 once it has run out of right * lines). `here` and `above` are the subsequence lengths at the current cell * and the one above it, kept as the walk moves so that only a change of row * counts bits. */ function walk(left, right, rows, top, row, column, pairs) { let i = row, j = column; if (i === top || !j) return j; const at = (k) => rows[k - top] ?? new Uint32Array(0); let here = commonLength(at(i), j), above = commonLength(at(i - 1), j); while (i > top && j) { const lower = at(i), upper = at(i - 1); if (left[i - 1] === right[j - 1]) { pairs.push([i - 1, j - 1]); here = above - 1 + bit(upper, j - 1); i--; j--; if (i > top) above = commonLength(at(i - 1), j); } else if (here - 1 + bit(lower, j - 1) >= above) { here -= 1 - bit(lower, j - 1); above -= 1 - bit(upper, j - 1); j--; } else { here = above; i--; if (i > top) above = commonLength(at(i - 1), j); } } return j; } /** Bit `k` of a row. */ function bit(row, k) { return ((row[k >>> 5] ?? 0) >>> (k & 31)) & 1; } /** The common-subsequence length a bit row gives `right[:upto]`. */ function commonLength(row, upto) { let ones = 0; for (let w = 0; w * 32 < upto; w++) { const keep = upto - w * 32; let bits = (row[w] ?? 0) & (keep >= 32 ? 0xffffffff : 0xffffffff >>> (32 - keep)); bits -= (bits >>> 1) & 0x55555555; bits = (bits & 0x33333333) + ((bits >>> 2) & 0x33333333); bits = (bits + (bits >>> 4)) & 0x0f0f0f0f; ones += Math.imul(bits, 0x01010101) >>> 24; } return upto - ones; } /** * The hunk header's trailing context, cut the way git cuts it: the first * 40 bytes up to a newline, stopping BEFORE the last non-blank byte, so the * context always loses its final byte. */ function funcname(line) { if (line === null) return ''; let head = ENC.encode(line).subarray(0, COMMENT_BYTES); const stop = head.findIndex((c) => c === 0x0a || c === 0x00); if (stop >= 0) head = head.subarray(0, stop); let end = 0; head.forEach((c, i) => { if (!GIT_SPACE.has(c)) end = i; }); return end ? ' ' + DEC.decode(head.subarray(0, end)) : ''; } function eol(line) { return line.endsWith('\n') ? line : line + '\n'; } //# sourceMappingURL=combined.js.map