@adecore/merge 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ import { type DiffDocument, type DiffGapRow, type DiffLineRow } from './diff-document.ts';
2
+ export interface DiffDisplayGap {
3
+ readonly before: number;
4
+ readonly row: DiffGapRow;
5
+ }
6
+ export interface DiffDisplay {
7
+ readonly text: string;
8
+ readonly lines: readonly string[];
9
+ readonly rows: readonly DiffLineRow[];
10
+ readonly gaps: readonly DiffDisplayGap[];
11
+ }
12
+ export declare function diffDisplay(document: DiffDocument, unfolded?: ReadonlyMap<number, number>): DiffDisplay;
@@ -0,0 +1,17 @@
1
+ import { diffRows } from './diff-document.js';
2
+ export function diffDisplay(document, unfolded) {
3
+ const lines = [];
4
+ const rows = [];
5
+ const gaps = [];
6
+ for (const row of diffRows(document, 'unified', unfolded)) {
7
+ if (row.kind === 'gap') {
8
+ gaps.push({ before: lines.length, row });
9
+ }
10
+ else {
11
+ const ref = row.new ?? row.old;
12
+ lines.push((row.new === undefined ? document.sides.old : document.sides.new)[ref.index]);
13
+ rows.push(row);
14
+ }
15
+ }
16
+ return { text: lines.join('\n'), lines, rows, gaps };
17
+ }
@@ -0,0 +1,47 @@
1
+ import type { Hunk, ParsedPatch } from './patch-parser.ts';
2
+ import { type TextShape } from './text.ts';
3
+ export interface DiffLineRef {
4
+ readonly index: number;
5
+ readonly number: number;
6
+ }
7
+ export interface DiffLineRow {
8
+ readonly kind: 'line';
9
+ readonly change: 'context' | 'delete' | 'insert' | 'pair';
10
+ readonly old?: DiffLineRef;
11
+ readonly new?: DiffLineRef;
12
+ readonly noNewline?: 'old' | 'new' | 'both';
13
+ readonly hunk?: number;
14
+ }
15
+ export interface DiffGapRow {
16
+ readonly kind: 'gap';
17
+ readonly id: number;
18
+ readonly oldStart: number;
19
+ readonly newStart: number;
20
+ readonly oldCount: number;
21
+ readonly newCount: number;
22
+ readonly unfoldable: boolean;
23
+ }
24
+ export type DiffRow = DiffLineRow | DiffGapRow;
25
+ export interface DiffDocument {
26
+ readonly hunks: readonly Hunk[];
27
+ readonly sides: {
28
+ readonly old: readonly string[];
29
+ readonly new: readonly string[];
30
+ };
31
+ readonly shapes?: {
32
+ readonly old: TextShape;
33
+ readonly new: TextShape;
34
+ };
35
+ readonly offsets: readonly {
36
+ readonly old: number;
37
+ readonly new: number;
38
+ }[];
39
+ readonly snapshots: boolean;
40
+ readonly ignoreWhitespace: boolean;
41
+ readonly partial: boolean;
42
+ }
43
+ export declare function diffDocument(parsed: ParsedPatch, contents: {
44
+ old: string;
45
+ new: string;
46
+ } | undefined, ignoreWhitespace?: boolean): DiffDocument;
47
+ export declare function diffRows(document: DiffDocument, layout: 'unified' | 'split', unfolded?: ReadonlyMap<number, number>): DiffRow[];
@@ -0,0 +1,199 @@
1
+ import { shapeOf, splitLines } from './text.js';
2
+ function sourceText(line) {
3
+ return line.noNewline ? line.text : line.text.replace(/\r$/, '');
4
+ }
5
+ function snapshotsFit(parsed, contents, ignoreWhitespace) {
6
+ if (parsed.partial || parsed.hunks.length === 0) {
7
+ return false;
8
+ }
9
+ const rawLines = (text) => {
10
+ if (text === '') {
11
+ return [];
12
+ }
13
+ const lines = text.split('\n');
14
+ if (text.endsWith('\n')) {
15
+ lines.pop();
16
+ }
17
+ return lines;
18
+ };
19
+ const oldLines = rawLines(contents.old);
20
+ const newLines = rawLines(contents.new);
21
+ const oldShape = shapeOf(contents.old);
22
+ const newShape = shapeOf(contents.new);
23
+ const same = (left, right) => (ignoreWhitespace ? left.replace(/\s+/g, '') === right.replace(/\s+/g, '') : left === right);
24
+ let oldAt = 0;
25
+ let newAt = 0;
26
+ const gapFits = (oldEnd, newEnd) => {
27
+ if (oldEnd - oldAt !== newEnd - newAt) {
28
+ return false;
29
+ }
30
+ while (oldAt < oldEnd && newAt < newEnd) {
31
+ if (!same(oldLines[oldAt], newLines[newAt])) {
32
+ return false;
33
+ }
34
+ oldAt += 1;
35
+ newAt += 1;
36
+ }
37
+ return true;
38
+ };
39
+ for (const hunk of parsed.hunks) {
40
+ if (hunk.oldStart + hunk.oldCount > oldLines.length || hunk.newStart + hunk.newCount > newLines.length || !gapFits(hunk.oldStart, hunk.newStart)) {
41
+ return false;
42
+ }
43
+ for (const line of hunk.lines) {
44
+ if (line.kind !== 'insert') {
45
+ if (!same(line.text, oldLines[oldAt]) || Boolean(line.noNewline) !== (oldAt === oldLines.length - 1 && !oldShape.finalNewline)) {
46
+ return false;
47
+ }
48
+ oldAt += 1;
49
+ }
50
+ if (line.kind !== 'delete') {
51
+ if (!same(line.text, newLines[newAt]) || Boolean(line.noNewline) !== (newAt === newLines.length - 1 && !newShape.finalNewline)) {
52
+ return false;
53
+ }
54
+ newAt += 1;
55
+ }
56
+ }
57
+ }
58
+ const endShown = oldAt === oldLines.length && newAt === newLines.length;
59
+ return gapFits(oldLines.length, newLines.length) && (oldShape.finalNewline === newShape.finalNewline || endShown);
60
+ }
61
+ export function diffDocument(parsed, contents, ignoreWhitespace = false) {
62
+ if (contents !== undefined && snapshotsFit(parsed, contents, ignoreWhitespace)) {
63
+ return {
64
+ hunks: parsed.hunks,
65
+ sides: { old: splitLines(contents.old), new: splitLines(contents.new) },
66
+ shapes: { old: shapeOf(contents.old), new: shapeOf(contents.new) },
67
+ offsets: parsed.hunks.map((hunk) => ({ old: hunk.oldStart, new: hunk.newStart })),
68
+ snapshots: true,
69
+ ignoreWhitespace,
70
+ partial: false
71
+ };
72
+ }
73
+ const old = [];
74
+ const fresh = [];
75
+ const offsets = parsed.hunks.map((hunk) => {
76
+ const offset = { old: old.length, new: fresh.length };
77
+ for (const line of hunk.lines) {
78
+ if (line.kind !== 'insert') {
79
+ old.push(sourceText(line));
80
+ }
81
+ if (line.kind !== 'delete') {
82
+ fresh.push(sourceText(line));
83
+ }
84
+ }
85
+ return offset;
86
+ });
87
+ return { hunks: parsed.hunks, sides: { old, new: fresh }, offsets, snapshots: false, ignoreWhitespace, partial: true };
88
+ }
89
+ function newline(old, fresh) {
90
+ return old || fresh ? { noNewline: old && fresh ? 'both' : old ? 'old' : 'new' } : {};
91
+ }
92
+ function hunkRows(document, index, layout) {
93
+ const hunk = document.hunks[index];
94
+ const offset = document.offsets[index];
95
+ let oldAt = 0;
96
+ let newAt = 0;
97
+ const rows = [];
98
+ const deletes = [];
99
+ const inserts = [];
100
+ const flush = () => {
101
+ for (let pair = 0; pair < Math.max(deletes.length, inserts.length); pair++) {
102
+ const old = deletes[pair];
103
+ const fresh = inserts[pair];
104
+ rows.push({
105
+ kind: 'line',
106
+ change: old && fresh ? 'pair' : old ? 'delete' : 'insert',
107
+ hunk: index,
108
+ ...(old ? { old: old.old } : {}),
109
+ ...(fresh ? { new: fresh.new } : {}),
110
+ ...newline(old?.noNewline === 'old', fresh?.noNewline === 'new')
111
+ });
112
+ }
113
+ deletes.length = 0;
114
+ inserts.length = 0;
115
+ };
116
+ for (const line of hunk.lines) {
117
+ const old = line.kind === 'insert' ? undefined : { index: offset.old + oldAt, number: hunk.oldStart + oldAt + 1 };
118
+ const fresh = line.kind === 'delete' ? undefined : { index: offset.new + newAt, number: hunk.newStart + newAt + 1 };
119
+ const row = {
120
+ kind: 'line',
121
+ change: line.kind,
122
+ hunk: index,
123
+ ...(old ? { old } : {}),
124
+ ...(fresh ? { new: fresh } : {}),
125
+ ...newline(Boolean(old && line.noNewline), Boolean(fresh && line.noNewline))
126
+ };
127
+ oldAt += old ? 1 : 0;
128
+ newAt += fresh ? 1 : 0;
129
+ if (layout === 'unified') {
130
+ rows.push(row);
131
+ }
132
+ else if (line.kind === 'context') {
133
+ flush();
134
+ rows.push(row);
135
+ }
136
+ else {
137
+ (line.kind === 'delete' ? deletes : inserts).push(row);
138
+ }
139
+ }
140
+ flush();
141
+ return rows;
142
+ }
143
+ export function diffRows(document, layout, unfolded = new Map()) {
144
+ const rows = [];
145
+ const gap = (id, oldStart, newStart, oldCount, newCount, position) => {
146
+ if (oldCount <= 0 && newCount <= 0) {
147
+ return;
148
+ }
149
+ oldCount = Math.max(0, oldCount);
150
+ newCount = Math.max(0, newCount);
151
+ const unfoldable = document.snapshots && oldCount === newCount;
152
+ const amount = unfolded.get(id) ?? 0;
153
+ const count = unfoldable
154
+ ? Math.min(oldCount, amount === Number.POSITIVE_INFINITY ? oldCount : Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0)
155
+ : 0;
156
+ const leading = position === 'before' ? 0 : count;
157
+ const trailing = position === 'after' ? 0 : Math.min(count, oldCount - leading);
158
+ const context = (from, to) => {
159
+ for (let line = from; line < to; line++) {
160
+ const old = oldStart + line;
161
+ const fresh = newStart + line;
162
+ rows.push({
163
+ kind: 'line',
164
+ change: 'context',
165
+ old: { index: old, number: old + 1 },
166
+ new: { index: fresh, number: fresh + 1 },
167
+ ...newline(old === document.sides.old.length - 1 && document.shapes?.old.finalNewline === false, fresh === document.sides.new.length - 1 && document.shapes?.new.finalNewline === false)
168
+ });
169
+ }
170
+ };
171
+ context(0, leading);
172
+ if (oldCount - leading - trailing > 0 || newCount - leading - trailing > 0) {
173
+ rows.push({
174
+ kind: 'gap',
175
+ id,
176
+ oldStart: oldStart + leading,
177
+ newStart: newStart + leading,
178
+ oldCount: Math.max(0, oldCount - leading - trailing),
179
+ newCount: Math.max(0, newCount - leading - trailing),
180
+ unfoldable
181
+ });
182
+ }
183
+ context(oldCount - trailing, oldCount);
184
+ };
185
+ let oldAt = 0;
186
+ let newAt = 0;
187
+ document.hunks.forEach((hunk, index) => {
188
+ gap(index, oldAt, newAt, hunk.oldStart - oldAt, hunk.newStart - newAt, index === 0 ? 'before' : 'between');
189
+ for (const row of hunkRows(document, index, layout)) {
190
+ rows.push(row);
191
+ }
192
+ oldAt = hunk.oldStart + hunk.oldCount;
193
+ newAt = hunk.newStart + hunk.newCount;
194
+ });
195
+ if (document.snapshots) {
196
+ gap(document.hunks.length, oldAt, newAt, document.sides.old.length - oldAt, document.sides.new.length - newAt, 'after');
197
+ }
198
+ return rows;
199
+ }
@@ -0,0 +1,17 @@
1
+ import { type DiffDocument, type DiffGapRow, type DiffLineRow } from './diff-document.ts';
2
+ import type { DiffDisplay } from './diff-display.ts';
3
+ export type DiffSplitRow = {
4
+ readonly kind: 'line';
5
+ readonly row: DiffLineRow;
6
+ readonly old?: number;
7
+ readonly new?: number;
8
+ } | {
9
+ readonly kind: 'gap';
10
+ readonly row: DiffGapRow;
11
+ };
12
+ export interface DiffSplit {
13
+ readonly old: DiffDisplay;
14
+ readonly new: DiffDisplay;
15
+ readonly aligned: readonly DiffSplitRow[];
16
+ }
17
+ export declare function diffSplit(document: DiffDocument, unfolded?: ReadonlyMap<number, number>): DiffSplit;
@@ -0,0 +1,31 @@
1
+ import { diffRows } from './diff-document.js';
2
+ /* Each side contains only source text; alignment rows never enter selection, copy or Find. */
3
+ export function diffSplit(document, unfolded) {
4
+ const old = { lines: [], rows: [], gaps: [] };
5
+ const fresh = { lines: [], rows: [], gaps: [] };
6
+ const aligned = [];
7
+ for (const row of diffRows(document, 'split', unfolded)) {
8
+ if (row.kind === 'gap') {
9
+ old.gaps.push({ before: old.lines.length, row });
10
+ fresh.gaps.push({ before: fresh.lines.length, row });
11
+ aligned.push({ kind: 'gap', row });
12
+ continue;
13
+ }
14
+ const entry = {
15
+ kind: 'line',
16
+ row,
17
+ old: row.old === undefined ? undefined : old.lines.length,
18
+ new: row.new === undefined ? undefined : fresh.lines.length
19
+ };
20
+ if (row.old !== undefined) {
21
+ old.lines.push(document.sides.old[row.old.index]);
22
+ old.rows.push({ ...row, new: undefined, noNewline: row.noNewline === 'new' ? undefined : row.noNewline });
23
+ }
24
+ if (row.new !== undefined) {
25
+ fresh.lines.push(document.sides.new[row.new.index]);
26
+ fresh.rows.push({ ...row, old: undefined, noNewline: row.noNewline === 'old' ? undefined : row.noNewline });
27
+ }
28
+ aligned.push(entry);
29
+ }
30
+ return { old: { ...old, text: old.lines.join('\n') }, new: { ...fresh, text: fresh.lines.join('\n') }, aligned };
31
+ }
package/dist/diff.d.ts CHANGED
@@ -5,3 +5,7 @@ export interface Change {
5
5
  otherEnd: number;
6
6
  }
7
7
  export declare function diffLines(base: readonly string[], other: readonly string[]): Change[];
8
+ export declare function diffChanges(base: readonly string[], other: readonly string[], maxWork?: number): {
9
+ changes: Change[];
10
+ limited: boolean;
11
+ };
package/dist/diff.js CHANGED
@@ -8,12 +8,21 @@ function cameDown(reach, offset, diagonal, distance) {
8
8
  return diagonal === -distance || (diagonal !== distance && reach[offset + diagonal - 1] < reach[offset + diagonal + 1]);
9
9
  }
10
10
  /* One round of the walk: every diagonal of this distance taken as far as its lines match. True once the far corner is reached. */
11
- function extend(left, right, reach, offset, distance) {
11
+ function extend(left, right, reach, offset, distance, work) {
12
12
  for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
13
+ if (work !== undefined && work.remaining-- <= 0) {
14
+ return null;
15
+ }
13
16
  const down = cameDown(reach, offset, diagonal, distance);
14
17
  let x = down ? reach[offset + diagonal + 1] : reach[offset + diagonal - 1] + 1;
15
18
  let y = x - diagonal;
16
- while (x < left.length && y < right.length && left[x] === right[y]) {
19
+ while (x < left.length && y < right.length) {
20
+ if (work !== undefined && work.remaining-- <= 0) {
21
+ return null;
22
+ }
23
+ if (left[x] !== right[y]) {
24
+ break;
25
+ }
17
26
  x += 1;
18
27
  y += 1;
19
28
  }
@@ -58,20 +67,26 @@ function backtrack(left, right, saved, distance, offset) {
58
67
  * STRIDE-th state a round started in, which is what the path is read back from once the far corner
59
68
  * is reached. Null once the distance passes what is worth walking.
60
69
  */
61
- function walk(left, right) {
70
+ function walk(left, right, maxWork) {
62
71
  const limit = Math.min(left.length + right.length, MAX_DISTANCE);
63
72
  const offset = limit;
64
73
  const reach = new Int32Array(2 * limit + 1);
65
74
  const saved = [];
75
+ // Replay visits each earlier round once; reserve half the budget for backtracking.
76
+ const work = maxWork === undefined ? undefined : { remaining: Math.floor(maxWork / 2) };
66
77
  for (let distance = 0; distance <= limit; distance += 1) {
67
78
  if (distance % STRIDE === 0) {
68
79
  saved.push(reach.slice());
69
80
  }
70
- if (extend(left, right, reach, offset, distance)) {
71
- return backtrack(left, right, saved, distance, offset);
81
+ const complete = extend(left, right, reach, offset, distance, work);
82
+ if (complete === null) {
83
+ return { moves: null, limited: true };
84
+ }
85
+ if (complete) {
86
+ return { moves: backtrack(left, right, saved, distance, offset), limited: false };
72
87
  }
73
88
  }
74
- return null;
89
+ return { moves: null, limited: false };
75
90
  }
76
91
  /* The stretches the moves cover, with edits that follow each other straight away folded into one. */
77
92
  function changesOf(moves) {
@@ -105,6 +120,9 @@ function changesOf(moves) {
105
120
  }
106
121
  /* Where `other` differs from `base`, in line stretches, in the order they appear. */
107
122
  export function diffLines(base, other) {
123
+ return diffChanges(base, other).changes;
124
+ }
125
+ export function diffChanges(base, other, maxWork) {
108
126
  let prefix = 0;
109
127
  while (prefix < base.length && prefix < other.length && base[prefix] === other[prefix]) {
110
128
  prefix += 1;
@@ -116,14 +134,17 @@ export function diffLines(base, other) {
116
134
  const left = base.slice(prefix, base.length - suffix);
117
135
  const right = other.slice(prefix, other.length - suffix);
118
136
  if (left.length === 0 && right.length === 0) {
119
- return [];
137
+ return { changes: [], limited: false };
120
138
  }
121
- const moves = left.length === 0 || right.length === 0 ? null : walk(left, right);
139
+ const { moves, limited } = left.length === 0 || right.length === 0 ? { moves: null, limited: false } : walk(left, right, maxWork);
122
140
  const changes = moves === null ? [{ baseStart: 0, baseEnd: left.length, otherStart: 0, otherEnd: right.length }] : changesOf(moves);
123
- return changes.map((change) => ({
124
- baseStart: change.baseStart + prefix,
125
- baseEnd: change.baseEnd + prefix,
126
- otherStart: change.otherStart + prefix,
127
- otherEnd: change.otherEnd + prefix
128
- }));
141
+ return {
142
+ changes: changes.map((change) => ({
143
+ baseStart: change.baseStart + prefix,
144
+ baseEnd: change.baseEnd + prefix,
145
+ otherStart: change.otherStart + prefix,
146
+ otherEnd: change.otherEnd + prefix
147
+ })),
148
+ limited
149
+ };
129
150
  }
package/dist/index.d.ts CHANGED
@@ -2,3 +2,10 @@ export { diffLines, type Change } from './diff.ts';
2
2
  export { openBlocks, splitBlocks, type MergeBlock, type MergeBlockKind } from './blocks.ts';
3
3
  export { autoLines, bothLines, draftOf, fingerprint, sideLines, wandLines, type MergeDraft, type MergeSide, type MergeSpan } from './resolve.ts';
4
4
  export { joinLines, shapeOf, splitLines, type TextShape } from './text.ts';
5
+ export { missingSides, patchFits } from './patch.ts';
6
+ export { parsePatch, parseHunkHeader, type HunkHeader, type Hunk, type ParsedPatch, type PatchLine } from './patch-parser.ts';
7
+ export { diffDocument, diffRows, type DiffDocument, type DiffGapRow, type DiffLineRef, type DiffLineRow, type DiffRow } from './diff-document.ts';
8
+ export { patchOf } from './patch-of.ts';
9
+ export { wordChanges, type DiffWordRange } from './word-changes.ts';
10
+ export { diffDisplay, type DiffDisplay, type DiffDisplayGap } from './diff-display.ts';
11
+ export { diffSplit, type DiffSplit, type DiffSplitRow } from './diff-split.ts';
package/dist/index.js CHANGED
@@ -2,3 +2,10 @@ export { diffLines } from './diff.js';
2
2
  export { openBlocks, splitBlocks } from './blocks.js';
3
3
  export { autoLines, bothLines, draftOf, fingerprint, sideLines, wandLines } from './resolve.js';
4
4
  export { joinLines, shapeOf, splitLines } from './text.js';
5
+ export { missingSides, patchFits } from './patch.js';
6
+ export { parsePatch, parseHunkHeader } from './patch-parser.js';
7
+ export { diffDocument, diffRows } from './diff-document.js';
8
+ export { patchOf } from './patch-of.js';
9
+ export { wordChanges } from './word-changes.js';
10
+ export { diffDisplay } from './diff-display.js';
11
+ export { diffSplit } from './diff-split.js';
@@ -0,0 +1,4 @@
1
+ import type { ParsedPatch } from './patch-parser.ts';
2
+ export declare function patchOf(oldText: string, newText: string, context?: number, options?: {
3
+ readonly maxWork?: number;
4
+ }): ParsedPatch;
@@ -0,0 +1,122 @@
1
+ import { diffChanges } from './diff.js';
2
+ import { shapeOf } from './text.js';
3
+ function fileLines(text) {
4
+ if (text === '') {
5
+ return [];
6
+ }
7
+ const lines = text.split('\n');
8
+ if (text.endsWith('\n')) {
9
+ lines.pop();
10
+ }
11
+ return lines;
12
+ }
13
+ function keysOf(lines, finalNewline) {
14
+ return lines.map((line, index) => `${index === lines.length - 1 && !finalNewline ? 'unterminated:' : 'terminated:'}${line}`);
15
+ }
16
+ function alignChanges(changes, old, fresh) {
17
+ for (const [index, change] of changes.entries()) {
18
+ const previous = changes[index - 1];
19
+ const oldBound = previous?.baseEnd ?? 0;
20
+ const newBound = previous?.otherEnd ?? 0;
21
+ const insert = change.baseStart === change.baseEnd;
22
+ const remove = change.otherStart === change.otherEnd;
23
+ if (!insert && !remove) {
24
+ continue;
25
+ }
26
+ // Move a repeated blank from the end of an edit to its start, between adjacent functions.
27
+ while (change.baseStart > oldBound && change.otherStart > newBound) {
28
+ const tail = insert ? fresh[change.otherEnd - 1] : old[change.baseEnd - 1];
29
+ if (tail.slice(tail.indexOf(':') + 1).trim() !== '' || old[change.baseStart - 1] !== tail || fresh[change.otherStart - 1] !== tail) {
30
+ break;
31
+ }
32
+ change.baseStart -= 1;
33
+ change.baseEnd -= 1;
34
+ change.otherStart -= 1;
35
+ change.otherEnd -= 1;
36
+ }
37
+ }
38
+ }
39
+ function rangeText(start, count) {
40
+ return `${start + (count === 0 ? 0 : 1)},${count}`;
41
+ }
42
+ export function patchOf(oldText, newText, context = 3, options) {
43
+ if (!Number.isSafeInteger(context) || context < 0) {
44
+ throw new RangeError('Patch context must be a nonnegative safe integer.');
45
+ }
46
+ const maxWork = options?.maxWork;
47
+ if (maxWork !== undefined && (!Number.isSafeInteger(maxWork) || maxWork < 0)) {
48
+ throw new RangeError('Patch work budget must be a nonnegative safe integer.');
49
+ }
50
+ const old = fileLines(oldText);
51
+ const fresh = fileLines(newText);
52
+ const oldShape = shapeOf(oldText);
53
+ const newShape = shapeOf(newText);
54
+ const oldKeys = keysOf(old, oldShape.finalNewline);
55
+ const newKeys = keysOf(fresh, newShape.finalNewline);
56
+ const { changes, limited } = diffChanges(oldKeys, newKeys, maxWork);
57
+ alignChanges(changes, oldKeys, newKeys);
58
+ const hunks = [];
59
+ let group = [];
60
+ const finish = () => {
61
+ if (group.length === 0) {
62
+ return;
63
+ }
64
+ const first = group[0];
65
+ const last = group.at(-1);
66
+ const before = Math.min(context, first.baseStart, first.otherStart);
67
+ const after = Math.min(context, old.length - last.baseEnd, fresh.length - last.otherEnd);
68
+ const oldStart = first.baseStart - before;
69
+ const newStart = first.otherStart - before;
70
+ const lines = [];
71
+ const line = (kind, at) => {
72
+ const source = kind === 'insert' ? fresh : old;
73
+ const shape = kind === 'insert' ? newShape : oldShape;
74
+ lines.push({ kind, text: source[at], ...(at === source.length - 1 && !shape.finalNewline ? { noNewline: true } : {}) });
75
+ };
76
+ let oldAt = oldStart;
77
+ for (const change of group) {
78
+ for (; oldAt < change.baseStart; oldAt++) {
79
+ line('context', oldAt);
80
+ }
81
+ for (; oldAt < change.baseEnd; oldAt++) {
82
+ line('delete', oldAt);
83
+ }
84
+ for (let newAt = change.otherStart; newAt < change.otherEnd; newAt++) {
85
+ line('insert', newAt);
86
+ }
87
+ }
88
+ for (; oldAt < last.baseEnd + after; oldAt++) {
89
+ line('context', oldAt);
90
+ }
91
+ const coarse = limited || group.some((change) => change.baseEnd - change.baseStart + change.otherEnd - change.otherStart > 4000);
92
+ hunks.push({
93
+ oldStart,
94
+ newStart,
95
+ oldCount: last.baseEnd + after - oldStart,
96
+ newCount: last.otherEnd + after - newStart,
97
+ section: '',
98
+ lines,
99
+ ...(coarse ? { coarse: true } : {})
100
+ });
101
+ group = [];
102
+ };
103
+ for (const change of changes) {
104
+ const previous = group.at(-1);
105
+ if (previous !== undefined && change.baseStart - previous.baseEnd > 2 * context) {
106
+ finish();
107
+ }
108
+ group.push(change);
109
+ }
110
+ finish();
111
+ const parts = [];
112
+ for (const hunk of hunks) {
113
+ parts.push(`@@ -${rangeText(hunk.oldStart, hunk.oldCount)} +${rangeText(hunk.newStart, hunk.newCount)} @@`);
114
+ for (const line of hunk.lines) {
115
+ parts.push(`${line.kind === 'context' ? ' ' : line.kind === 'delete' ? '-' : '+'}${line.text}`);
116
+ if (line.noNewline) {
117
+ parts.push('\');
118
+ }
119
+ }
120
+ }
121
+ return { text: parts.length === 0 ? '' : `${parts.join('\n')}\n`, hunks, missing: { old: false, new: false }, partial: false };
122
+ }
@@ -0,0 +1,32 @@
1
+ export interface PatchLine {
2
+ readonly kind: 'context' | 'delete' | 'insert';
3
+ readonly text: string;
4
+ readonly noNewline?: true;
5
+ }
6
+ export interface Hunk {
7
+ readonly oldStart: number;
8
+ readonly oldCount: number;
9
+ readonly newStart: number;
10
+ readonly newCount: number;
11
+ readonly section: string;
12
+ readonly coarse?: true;
13
+ readonly lines: readonly PatchLine[];
14
+ }
15
+ export interface ParsedPatch {
16
+ readonly text: string;
17
+ readonly hunks: readonly Hunk[];
18
+ readonly missing: {
19
+ readonly old: boolean;
20
+ readonly new: boolean;
21
+ };
22
+ readonly partial: boolean;
23
+ }
24
+ export interface HunkHeader {
25
+ readonly oldStart: number;
26
+ readonly oldCount: number;
27
+ readonly newStart: number;
28
+ readonly newCount: number;
29
+ readonly section: string;
30
+ }
31
+ export declare function parseHunkHeader(line: string): HunkHeader | undefined;
32
+ export declare function parsePatch(patch: string): ParsedPatch;