@adecore/merge 0.38.0 → 0.40.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.
- package/dist/blocks.d.ts +1 -0
- package/dist/blocks.js +1 -1
- package/dist/diff-document.js +4 -14
- package/dist/patch-of.js +3 -13
- package/dist/patch.js +3 -4
- package/dist/resolve.d.ts +1 -1
- package/dist/resolve.js +7 -6
- package/dist/text.d.ts +2 -0
- package/dist/text.js +17 -0
- package/package.json +3 -3
- package/src/blocks.ts +15 -1
- package/src/diff-display.ts +2 -0
- package/src/diff-document.ts +46 -14
- package/src/diff-split.ts +7 -0
- package/src/diff.ts +29 -0
- package/src/patch-of.ts +29 -14
- package/src/patch-parser.ts +31 -0
- package/src/patch.ts +15 -4
- package/src/resolve.ts +22 -7
- package/src/text.ts +26 -0
- package/src/word-changes.ts +6 -0
package/dist/blocks.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export interface MergeBlock {
|
|
|
5
5
|
ours: string[];
|
|
6
6
|
theirs: string[];
|
|
7
7
|
}
|
|
8
|
+
export declare function same(left: readonly string[], right: readonly string[]): boolean;
|
|
8
9
|
export declare function splitBlocks(base: readonly string[], ours: readonly string[], theirs: readonly string[]): MergeBlock[];
|
package/dist/blocks.js
CHANGED
package/dist/diff-document.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shapeOf, splitLines } from './text.js';
|
|
1
|
+
import { lineMatcher, rawLines, shapeOf, splitLines } from './text.js';
|
|
2
2
|
function sourceText(line) {
|
|
3
3
|
return line.noNewline ? line.text : line.text.replace(/\r$/, '');
|
|
4
4
|
}
|
|
@@ -6,21 +6,11 @@ function snapshotsFit(parsed, contents, ignoreWhitespace) {
|
|
|
6
6
|
if (parsed.partial || parsed.hunks.length === 0) {
|
|
7
7
|
return false;
|
|
8
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
9
|
const oldLines = rawLines(contents.old);
|
|
20
10
|
const newLines = rawLines(contents.new);
|
|
21
11
|
const oldShape = shapeOf(contents.old);
|
|
22
12
|
const newShape = shapeOf(contents.new);
|
|
23
|
-
const same = (
|
|
13
|
+
const same = lineMatcher(ignoreWhitespace);
|
|
24
14
|
let oldAt = 0;
|
|
25
15
|
let newAt = 0;
|
|
26
16
|
const gapFits = (oldEnd, newEnd) => {
|
|
@@ -184,14 +174,14 @@ export function diffRows(document, layout, unfolded = new Map()) {
|
|
|
184
174
|
};
|
|
185
175
|
let oldAt = 0;
|
|
186
176
|
let newAt = 0;
|
|
187
|
-
document.hunks.
|
|
177
|
+
for (const [index, hunk] of document.hunks.entries()) {
|
|
188
178
|
gap(index, oldAt, newAt, hunk.oldStart - oldAt, hunk.newStart - newAt, index === 0 ? 'before' : 'between');
|
|
189
179
|
for (const row of hunkRows(document, index, layout)) {
|
|
190
180
|
rows.push(row);
|
|
191
181
|
}
|
|
192
182
|
oldAt = hunk.oldStart + hunk.oldCount;
|
|
193
183
|
newAt = hunk.newStart + hunk.newCount;
|
|
194
|
-
}
|
|
184
|
+
}
|
|
195
185
|
if (document.snapshots) {
|
|
196
186
|
gap(document.hunks.length, oldAt, newAt, document.sides.old.length - oldAt, document.sides.new.length - newAt, 'after');
|
|
197
187
|
}
|
package/dist/patch-of.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
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
|
-
}
|
|
2
|
+
import { rawLines, shapeOf } from './text.js';
|
|
13
3
|
function keysOf(lines, finalNewline) {
|
|
14
4
|
return lines.map((line, index) => `${index === lines.length - 1 && !finalNewline ? 'unterminated:' : 'terminated:'}${line}`);
|
|
15
5
|
}
|
|
@@ -47,8 +37,8 @@ export function patchOf(oldText, newText, context = 3, options) {
|
|
|
47
37
|
if (maxWork !== undefined && (!Number.isSafeInteger(maxWork) || maxWork < 0)) {
|
|
48
38
|
throw new RangeError('Patch work budget must be a nonnegative safe integer.');
|
|
49
39
|
}
|
|
50
|
-
const old =
|
|
51
|
-
const fresh =
|
|
40
|
+
const old = rawLines(oldText);
|
|
41
|
+
const fresh = rawLines(newText);
|
|
52
42
|
const oldShape = shapeOf(oldText);
|
|
53
43
|
const newShape = shapeOf(newText);
|
|
54
44
|
const oldKeys = keysOf(old, oldShape.finalNewline);
|
package/dist/patch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parsePatch } from './patch-parser.js';
|
|
2
|
+
import { lineMatcher } from './text.js';
|
|
2
3
|
/* Which side a patch says the file is missing on, read from its header so no line of the file can pass for it. */
|
|
3
4
|
export function missingSides(patch) {
|
|
4
5
|
const start = patch.search(/^@@/m);
|
|
@@ -6,16 +7,14 @@ export function missingSides(patch) {
|
|
|
6
7
|
return { old: /^--- \/dev\/null$/m.test(header), new: /^\+\+\+ \/dev\/null$/m.test(header) };
|
|
7
8
|
}
|
|
8
9
|
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
9
|
-
/*
|
|
10
|
+
/* A patch cut off before its hunks end fits nothing. */
|
|
10
11
|
export function patchFits(patch, oldText, newText, ignoreWhitespace) {
|
|
11
12
|
if (parsePatch(patch).partial) {
|
|
12
13
|
return false;
|
|
13
14
|
}
|
|
14
15
|
const oldLines = oldText.split('\n');
|
|
15
16
|
const newLines = newText.split('\n');
|
|
16
|
-
const same = ignoreWhitespace
|
|
17
|
-
? (shown, actual) => actual !== undefined && shown.replace(/\s+/g, '') === actual.replace(/\s+/g, '')
|
|
18
|
-
: (shown, actual) => shown === actual;
|
|
17
|
+
const same = lineMatcher(ignoreWhitespace);
|
|
19
18
|
let oldAt = -1;
|
|
20
19
|
let newAt = -1;
|
|
21
20
|
let hunks = 0;
|
package/dist/resolve.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MergeBlock } from './blocks.ts';
|
|
2
2
|
export type MergeSide = 'ours' | 'theirs';
|
|
3
3
|
export declare function sideLines(block: MergeBlock, side: MergeSide): string[];
|
|
4
4
|
export declare function bothLines(block: MergeBlock, first: MergeSide): string[];
|
package/dist/resolve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { same } from './blocks.js';
|
|
1
2
|
export function sideLines(block, side) {
|
|
2
|
-
return
|
|
3
|
+
return [...block[side]];
|
|
3
4
|
}
|
|
4
5
|
/* Both sides, one after the other, for the conflict where the two additions both belong. */
|
|
5
6
|
export function bothLines(block, first) {
|
|
@@ -32,8 +33,11 @@ function runIn(outer, inner) {
|
|
|
32
33
|
* what `outer` adds is one `inner` deleted, and taking `outer` would put it back.
|
|
33
34
|
*/
|
|
34
35
|
function covers(base, outer, inner) {
|
|
36
|
+
if (inner.length === 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
35
39
|
const start = runIn(outer, inner);
|
|
36
|
-
if (
|
|
40
|
+
if (start < 0) {
|
|
37
41
|
return false;
|
|
38
42
|
}
|
|
39
43
|
const added = [...outer.slice(0, start), ...outer.slice(start + inner.length)];
|
|
@@ -48,10 +52,7 @@ export function wandLines(block) {
|
|
|
48
52
|
if (block.kind !== 'conflict') {
|
|
49
53
|
return autoLines(block);
|
|
50
54
|
}
|
|
51
|
-
if (block.ours
|
|
52
|
-
return [...block.ours];
|
|
53
|
-
}
|
|
54
|
-
if (covers(block.base, block.ours, block.theirs)) {
|
|
55
|
+
if (same(block.ours, block.theirs) || covers(block.base, block.ours, block.theirs)) {
|
|
55
56
|
return [...block.ours];
|
|
56
57
|
}
|
|
57
58
|
if (covers(block.base, block.theirs, block.ours)) {
|
package/dist/text.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ export interface TextShape {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function shapeOf(text: string): TextShape;
|
|
6
6
|
export declare function splitLines(text: string): string[];
|
|
7
|
+
export declare function rawLines(text: string): string[];
|
|
8
|
+
export declare function lineMatcher(ignoreWhitespace: boolean): (shown: string, actual: string | undefined) => boolean;
|
|
7
9
|
export declare function joinLines(lines: readonly string[], shape: TextShape): string;
|
package/dist/text.js
CHANGED
|
@@ -17,6 +17,23 @@ export function splitLines(text) {
|
|
|
17
17
|
}
|
|
18
18
|
return lines;
|
|
19
19
|
}
|
|
20
|
+
/* The lines of a file split on `\n` alone, so a `\r` stays part of its line. */
|
|
21
|
+
export function rawLines(text) {
|
|
22
|
+
if (text === '') {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const lines = text.split('\n');
|
|
26
|
+
if (text.endsWith('\n')) {
|
|
27
|
+
lines.pop();
|
|
28
|
+
}
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
/* Git's --ignore-all-space may show either side's spacing, so a shown line matches by its non-space characters. */
|
|
32
|
+
export function lineMatcher(ignoreWhitespace) {
|
|
33
|
+
return ignoreWhitespace
|
|
34
|
+
? (shown, actual) => actual !== undefined && shown.replace(/\s+/g, '') === actual.replace(/\s+/g, '')
|
|
35
|
+
: (shown, actual) => shown === actual;
|
|
36
|
+
}
|
|
20
37
|
export function joinLines(lines, shape) {
|
|
21
38
|
return lines.length === 0 ? '' : lines.join(shape.eol) + (shape.finalNewline ? shape.eol : '');
|
|
22
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/merge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Pure line diffs, three-way merge blocks and conflict resolution.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"test:memory": "bun --config=scripts/memory.bunfig.toml test src/diff-memory.integration.test.ts"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/bun": "^1.4.
|
|
40
|
-
"@types/node": "^24.
|
|
39
|
+
"@types/bun": "^1.4.4",
|
|
40
|
+
"@types/node": "^24.19.2",
|
|
41
41
|
"typescript": "^7.0.2"
|
|
42
42
|
}
|
|
43
43
|
}
|
package/src/blocks.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface MergeBlock {
|
|
|
14
14
|
theirs: string[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function same(left: readonly string[], right: readonly string[]): boolean {
|
|
17
|
+
export function same(left: readonly string[], right: readonly string[]): boolean {
|
|
18
18
|
return left.length === right.length && left.every((line, index) => line === right[index]);
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -22,12 +22,15 @@ function kindOf(base: readonly string[], ours: readonly string[], theirs: readon
|
|
|
22
22
|
if (same(ours, theirs)) {
|
|
23
23
|
return same(base, ours) ? 'stable' : 'both';
|
|
24
24
|
}
|
|
25
|
+
|
|
25
26
|
if (same(base, ours)) {
|
|
26
27
|
return 'theirs';
|
|
27
28
|
}
|
|
29
|
+
|
|
28
30
|
if (same(base, theirs)) {
|
|
29
31
|
return 'ours';
|
|
30
32
|
}
|
|
33
|
+
|
|
31
34
|
return 'conflict';
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -54,9 +57,11 @@ export function splitBlocks(base: readonly string[], ours: readonly string[], th
|
|
|
54
57
|
|
|
55
58
|
const push = (baseEnd: number, ourEnd: number, theirEnd: number): void => {
|
|
56
59
|
const stretch = { base: base.slice(baseAt, baseEnd), ours: ours.slice(ourAt, ourEnd), theirs: theirs.slice(theirAt, theirEnd) };
|
|
60
|
+
|
|
57
61
|
if (stretch.base.length > 0 || stretch.ours.length > 0 || stretch.theirs.length > 0) {
|
|
58
62
|
blocks.push({ kind: kindOf(stretch.base, stretch.ours, stretch.theirs), ...stretch });
|
|
59
63
|
}
|
|
64
|
+
|
|
60
65
|
baseAt = baseEnd;
|
|
61
66
|
ourAt = ourEnd;
|
|
62
67
|
theirAt = theirEnd;
|
|
@@ -64,19 +69,24 @@ export function splitBlocks(base: readonly string[], ours: readonly string[], th
|
|
|
64
69
|
|
|
65
70
|
while (ourNext < ourChanges.length || theirNext < theirChanges.length) {
|
|
66
71
|
const start = Math.min(ourChanges[ourNext]?.baseStart ?? Infinity, theirChanges[theirNext]?.baseStart ?? Infinity);
|
|
72
|
+
|
|
67
73
|
if (start > baseAt) {
|
|
68
74
|
const length = start - baseAt;
|
|
69
75
|
push(start, ourAt + length, theirAt + length);
|
|
70
76
|
}
|
|
77
|
+
|
|
71
78
|
let end = baseAt;
|
|
72
79
|
let ourGrowth = 0;
|
|
73
80
|
let theirGrowth = 0;
|
|
81
|
+
|
|
74
82
|
/* The stretch grows while a change of either side reaches into it: two changes that overlap
|
|
75
83
|
in the base are one thing a person has to look at, however far apart they started. */
|
|
76
84
|
for (let taken = true; taken;) {
|
|
77
85
|
taken = false;
|
|
86
|
+
|
|
78
87
|
const reaches = (change: Change | undefined): boolean =>
|
|
79
88
|
change !== undefined && change.baseStart <= end && (change.baseStart < end || end === baseAt);
|
|
89
|
+
|
|
80
90
|
while (reaches(ourChanges[ourNext])) {
|
|
81
91
|
const change = ourChanges[ourNext]!;
|
|
82
92
|
end = Math.max(end, change.baseEnd);
|
|
@@ -84,6 +94,7 @@ export function splitBlocks(base: readonly string[], ours: readonly string[], th
|
|
|
84
94
|
ourNext += 1;
|
|
85
95
|
taken = true;
|
|
86
96
|
}
|
|
97
|
+
|
|
87
98
|
while (reaches(theirChanges[theirNext])) {
|
|
88
99
|
const change = theirChanges[theirNext]!;
|
|
89
100
|
end = Math.max(end, change.baseEnd);
|
|
@@ -92,8 +103,11 @@ export function splitBlocks(base: readonly string[], ours: readonly string[], th
|
|
|
92
103
|
taken = true;
|
|
93
104
|
}
|
|
94
105
|
}
|
|
106
|
+
|
|
95
107
|
push(end, ourAt + (end - baseAt) + ourGrowth, theirAt + (end - baseAt) + theirGrowth);
|
|
96
108
|
}
|
|
109
|
+
|
|
97
110
|
push(base.length, ours.length, theirs.length);
|
|
111
|
+
|
|
98
112
|
return blocks;
|
|
99
113
|
}
|
package/src/diff-display.ts
CHANGED
|
@@ -18,6 +18,7 @@ export function diffDisplay(document: DiffDocument, unfolded?: ReadonlyMap<numbe
|
|
|
18
18
|
const lines: string[] = [];
|
|
19
19
|
const rows: DiffLineRow[] = [];
|
|
20
20
|
const gaps: DiffDisplayGap[] = [];
|
|
21
|
+
|
|
21
22
|
for (const row of diffRows(document, 'unified', unfolded)) {
|
|
22
23
|
if (row.kind === 'gap') {
|
|
23
24
|
gaps.push({ before: lines.length, row });
|
|
@@ -27,5 +28,6 @@ export function diffDisplay(document: DiffDocument, unfolded?: ReadonlyMap<numbe
|
|
|
27
28
|
rows.push(row);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
31
|
+
|
|
30
32
|
return { text: lines.join('\n'), lines, rows, gaps };
|
|
31
33
|
}
|
package/src/diff-document.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Hunk, ParsedPatch, PatchLine } from './patch-parser.ts';
|
|
2
|
-
import { shapeOf, splitLines, type TextShape } from './text.ts';
|
|
2
|
+
import { lineMatcher, rawLines, shapeOf, splitLines, type TextShape } from './text.ts';
|
|
3
3
|
|
|
4
4
|
export interface DiffLineRef {
|
|
5
5
|
readonly index: number;
|
|
@@ -45,56 +45,58 @@ function snapshotsFit(parsed: ParsedPatch, contents: { old: string; new: string
|
|
|
45
45
|
if (parsed.partial || parsed.hunks.length === 0) {
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
if (text === '') {
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
const lines = text.split('\n');
|
|
53
|
-
if (text.endsWith('\n')) {
|
|
54
|
-
lines.pop();
|
|
55
|
-
}
|
|
56
|
-
return lines;
|
|
57
|
-
};
|
|
48
|
+
|
|
58
49
|
const oldLines = rawLines(contents.old);
|
|
59
50
|
const newLines = rawLines(contents.new);
|
|
60
51
|
const oldShape = shapeOf(contents.old);
|
|
61
52
|
const newShape = shapeOf(contents.new);
|
|
62
|
-
const same = (
|
|
53
|
+
const same = lineMatcher(ignoreWhitespace);
|
|
63
54
|
let oldAt = 0;
|
|
64
55
|
let newAt = 0;
|
|
56
|
+
|
|
65
57
|
const gapFits = (oldEnd: number, newEnd: number): boolean => {
|
|
66
58
|
if (oldEnd - oldAt !== newEnd - newAt) {
|
|
67
59
|
return false;
|
|
68
60
|
}
|
|
61
|
+
|
|
69
62
|
while (oldAt < oldEnd && newAt < newEnd) {
|
|
70
63
|
if (!same(oldLines[oldAt]!, newLines[newAt]!)) {
|
|
71
64
|
return false;
|
|
72
65
|
}
|
|
66
|
+
|
|
73
67
|
oldAt += 1;
|
|
74
68
|
newAt += 1;
|
|
75
69
|
}
|
|
70
|
+
|
|
76
71
|
return true;
|
|
77
72
|
};
|
|
73
|
+
|
|
78
74
|
for (const hunk of parsed.hunks) {
|
|
79
75
|
if (hunk.oldStart + hunk.oldCount > oldLines.length || hunk.newStart + hunk.newCount > newLines.length || !gapFits(hunk.oldStart, hunk.newStart)) {
|
|
80
76
|
return false;
|
|
81
77
|
}
|
|
78
|
+
|
|
82
79
|
for (const line of hunk.lines) {
|
|
83
80
|
if (line.kind !== 'insert') {
|
|
84
81
|
if (!same(line.text, oldLines[oldAt]!) || Boolean(line.noNewline) !== (oldAt === oldLines.length - 1 && !oldShape.finalNewline)) {
|
|
85
82
|
return false;
|
|
86
83
|
}
|
|
84
|
+
|
|
87
85
|
oldAt += 1;
|
|
88
86
|
}
|
|
87
|
+
|
|
89
88
|
if (line.kind !== 'delete') {
|
|
90
89
|
if (!same(line.text, newLines[newAt]!) || Boolean(line.noNewline) !== (newAt === newLines.length - 1 && !newShape.finalNewline)) {
|
|
91
90
|
return false;
|
|
92
91
|
}
|
|
92
|
+
|
|
93
93
|
newAt += 1;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
|
|
97
98
|
const endShown = oldAt === oldLines.length && newAt === newLines.length;
|
|
99
|
+
|
|
98
100
|
return gapFits(oldLines.length, newLines.length) && (oldShape.finalNewline === newShape.finalNewline || endShown);
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -110,20 +112,26 @@ export function diffDocument(parsed: ParsedPatch, contents: { old: string; new:
|
|
|
110
112
|
partial: false
|
|
111
113
|
};
|
|
112
114
|
}
|
|
115
|
+
|
|
113
116
|
const old: string[] = [];
|
|
114
117
|
const fresh: string[] = [];
|
|
118
|
+
|
|
115
119
|
const offsets = parsed.hunks.map((hunk) => {
|
|
116
120
|
const offset = { old: old.length, new: fresh.length };
|
|
121
|
+
|
|
117
122
|
for (const line of hunk.lines) {
|
|
118
123
|
if (line.kind !== 'insert') {
|
|
119
124
|
old.push(sourceText(line));
|
|
120
125
|
}
|
|
126
|
+
|
|
121
127
|
if (line.kind !== 'delete') {
|
|
122
128
|
fresh.push(sourceText(line));
|
|
123
129
|
}
|
|
124
130
|
}
|
|
131
|
+
|
|
125
132
|
return offset;
|
|
126
133
|
});
|
|
134
|
+
|
|
127
135
|
return { hunks: parsed.hunks, sides: { old, new: fresh }, offsets, snapshots: false, ignoreWhitespace, partial: true };
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -139,10 +147,12 @@ function hunkRows(document: DiffDocument, index: number, layout: 'unified' | 'sp
|
|
|
139
147
|
const rows: DiffLineRow[] = [];
|
|
140
148
|
const deletes: DiffLineRow[] = [];
|
|
141
149
|
const inserts: DiffLineRow[] = [];
|
|
150
|
+
|
|
142
151
|
const flush = (): void => {
|
|
143
152
|
for (let pair = 0; pair < Math.max(deletes.length, inserts.length); pair++) {
|
|
144
153
|
const old = deletes[pair];
|
|
145
154
|
const fresh = inserts[pair];
|
|
155
|
+
|
|
146
156
|
rows.push({
|
|
147
157
|
kind: 'line',
|
|
148
158
|
change: old && fresh ? 'pair' : old ? 'delete' : 'insert',
|
|
@@ -152,12 +162,15 @@ function hunkRows(document: DiffDocument, index: number, layout: 'unified' | 'sp
|
|
|
152
162
|
...newline(old?.noNewline === 'old', fresh?.noNewline === 'new')
|
|
153
163
|
});
|
|
154
164
|
}
|
|
165
|
+
|
|
155
166
|
deletes.length = 0;
|
|
156
167
|
inserts.length = 0;
|
|
157
168
|
};
|
|
169
|
+
|
|
158
170
|
for (const line of hunk.lines) {
|
|
159
171
|
const old = line.kind === 'insert' ? undefined : { index: offset.old + oldAt, number: hunk.oldStart + oldAt + 1 };
|
|
160
172
|
const fresh = line.kind === 'delete' ? undefined : { index: offset.new + newAt, number: hunk.newStart + newAt + 1 };
|
|
173
|
+
|
|
161
174
|
const row: DiffLineRow = {
|
|
162
175
|
kind: 'line',
|
|
163
176
|
change: line.kind,
|
|
@@ -166,8 +179,10 @@ function hunkRows(document: DiffDocument, index: number, layout: 'unified' | 'sp
|
|
|
166
179
|
...(fresh ? { new: fresh } : {}),
|
|
167
180
|
...newline(Boolean(old && line.noNewline), Boolean(fresh && line.noNewline))
|
|
168
181
|
};
|
|
182
|
+
|
|
169
183
|
oldAt += old ? 1 : 0;
|
|
170
184
|
newAt += fresh ? 1 : 0;
|
|
185
|
+
|
|
171
186
|
if (layout === 'unified') {
|
|
172
187
|
rows.push(row);
|
|
173
188
|
} else if (line.kind === 'context') {
|
|
@@ -177,29 +192,37 @@ function hunkRows(document: DiffDocument, index: number, layout: 'unified' | 'sp
|
|
|
177
192
|
(line.kind === 'delete' ? deletes : inserts).push(row);
|
|
178
193
|
}
|
|
179
194
|
}
|
|
195
|
+
|
|
180
196
|
flush();
|
|
197
|
+
|
|
181
198
|
return rows;
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
export function diffRows(document: DiffDocument, layout: 'unified' | 'split', unfolded: ReadonlyMap<number, number> = new Map()): DiffRow[] {
|
|
185
202
|
const rows: DiffRow[] = [];
|
|
203
|
+
|
|
186
204
|
const gap = (id: number, oldStart: number, newStart: number, oldCount: number, newCount: number, position: 'before' | 'between' | 'after'): void => {
|
|
187
205
|
if (oldCount <= 0 && newCount <= 0) {
|
|
188
206
|
return;
|
|
189
207
|
}
|
|
208
|
+
|
|
190
209
|
oldCount = Math.max(0, oldCount);
|
|
191
210
|
newCount = Math.max(0, newCount);
|
|
192
211
|
const unfoldable = document.snapshots && oldCount === newCount;
|
|
193
212
|
const amount = unfolded.get(id) ?? 0;
|
|
213
|
+
|
|
194
214
|
const count = unfoldable
|
|
195
215
|
? Math.min(oldCount, amount === Number.POSITIVE_INFINITY ? oldCount : Number.isFinite(amount) ? Math.max(0, Math.floor(amount)) : 0)
|
|
196
216
|
: 0;
|
|
217
|
+
|
|
197
218
|
const leading = position === 'before' ? 0 : count;
|
|
198
219
|
const trailing = position === 'after' ? 0 : Math.min(count, oldCount - leading);
|
|
220
|
+
|
|
199
221
|
const context = (from: number, to: number): void => {
|
|
200
222
|
for (let line = from; line < to; line++) {
|
|
201
223
|
const old = oldStart + line;
|
|
202
224
|
const fresh = newStart + line;
|
|
225
|
+
|
|
203
226
|
rows.push({
|
|
204
227
|
kind: 'line',
|
|
205
228
|
change: 'context',
|
|
@@ -212,7 +235,9 @@ export function diffRows(document: DiffDocument, layout: 'unified' | 'split', un
|
|
|
212
235
|
});
|
|
213
236
|
}
|
|
214
237
|
};
|
|
238
|
+
|
|
215
239
|
context(0, leading);
|
|
240
|
+
|
|
216
241
|
if (oldCount - leading - trailing > 0 || newCount - leading - trailing > 0) {
|
|
217
242
|
rows.push({
|
|
218
243
|
kind: 'gap',
|
|
@@ -224,20 +249,27 @@ export function diffRows(document: DiffDocument, layout: 'unified' | 'split', un
|
|
|
224
249
|
unfoldable
|
|
225
250
|
});
|
|
226
251
|
}
|
|
252
|
+
|
|
227
253
|
context(oldCount - trailing, oldCount);
|
|
228
254
|
};
|
|
255
|
+
|
|
229
256
|
let oldAt = 0;
|
|
230
257
|
let newAt = 0;
|
|
231
|
-
|
|
258
|
+
|
|
259
|
+
for (const [index, hunk] of document.hunks.entries()) {
|
|
232
260
|
gap(index, oldAt, newAt, hunk.oldStart - oldAt, hunk.newStart - newAt, index === 0 ? 'before' : 'between');
|
|
261
|
+
|
|
233
262
|
for (const row of hunkRows(document, index, layout)) {
|
|
234
263
|
rows.push(row);
|
|
235
264
|
}
|
|
265
|
+
|
|
236
266
|
oldAt = hunk.oldStart + hunk.oldCount;
|
|
237
267
|
newAt = hunk.newStart + hunk.newCount;
|
|
238
|
-
}
|
|
268
|
+
}
|
|
269
|
+
|
|
239
270
|
if (document.snapshots) {
|
|
240
271
|
gap(document.hunks.length, oldAt, newAt, document.sides.old.length - oldAt, document.sides.new.length - newAt, 'after');
|
|
241
272
|
}
|
|
273
|
+
|
|
242
274
|
return rows;
|
|
243
275
|
}
|
package/src/diff-split.ts
CHANGED
|
@@ -16,28 +16,35 @@ export function diffSplit(document: DiffDocument, unfolded?: ReadonlyMap<number,
|
|
|
16
16
|
const old = { lines: [] as string[], rows: [] as DiffLineRow[], gaps: [] as DiffDisplay['gaps'][number][] };
|
|
17
17
|
const fresh = { lines: [] as string[], rows: [] as DiffLineRow[], gaps: [] as DiffDisplay['gaps'][number][] };
|
|
18
18
|
const aligned: DiffSplitRow[] = [];
|
|
19
|
+
|
|
19
20
|
for (const row of diffRows(document, 'split', unfolded)) {
|
|
20
21
|
if (row.kind === 'gap') {
|
|
21
22
|
old.gaps.push({ before: old.lines.length, row });
|
|
22
23
|
fresh.gaps.push({ before: fresh.lines.length, row });
|
|
23
24
|
aligned.push({ kind: 'gap', row });
|
|
25
|
+
|
|
24
26
|
continue;
|
|
25
27
|
}
|
|
28
|
+
|
|
26
29
|
const entry = {
|
|
27
30
|
kind: 'line' as const,
|
|
28
31
|
row,
|
|
29
32
|
old: row.old === undefined ? undefined : old.lines.length,
|
|
30
33
|
new: row.new === undefined ? undefined : fresh.lines.length
|
|
31
34
|
};
|
|
35
|
+
|
|
32
36
|
if (row.old !== undefined) {
|
|
33
37
|
old.lines.push(document.sides.old[row.old.index]!);
|
|
34
38
|
old.rows.push({ ...row, new: undefined, noNewline: row.noNewline === 'new' ? undefined : row.noNewline });
|
|
35
39
|
}
|
|
40
|
+
|
|
36
41
|
if (row.new !== undefined) {
|
|
37
42
|
fresh.lines.push(document.sides.new[row.new.index]!);
|
|
38
43
|
fresh.rows.push({ ...row, old: undefined, noNewline: row.noNewline === 'old' ? undefined : row.noNewline });
|
|
39
44
|
}
|
|
45
|
+
|
|
40
46
|
aligned.push(entry);
|
|
41
47
|
}
|
|
48
|
+
|
|
42
49
|
return { old: { ...old, text: old.lines.join('\n') }, new: { ...fresh, text: fresh.lines.join('\n') }, aligned };
|
|
43
50
|
}
|
package/src/diff.ts
CHANGED
|
@@ -39,24 +39,31 @@ function extend(left: readonly string[], right: readonly string[], reach: Int32A
|
|
|
39
39
|
if (work !== undefined && work.remaining-- <= 0) {
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
+
|
|
42
43
|
const down = cameDown(reach, offset, diagonal, distance);
|
|
43
44
|
let x = down ? reach[offset + diagonal + 1]! : reach[offset + diagonal - 1]! + 1;
|
|
44
45
|
let y = x - diagonal;
|
|
46
|
+
|
|
45
47
|
while (x < left.length && y < right.length) {
|
|
46
48
|
if (work !== undefined && work.remaining-- <= 0) {
|
|
47
49
|
return null;
|
|
48
50
|
}
|
|
51
|
+
|
|
49
52
|
if (left[x] !== right[y]) {
|
|
50
53
|
break;
|
|
51
54
|
}
|
|
55
|
+
|
|
52
56
|
x += 1;
|
|
53
57
|
y += 1;
|
|
54
58
|
}
|
|
59
|
+
|
|
55
60
|
reach[offset + diagonal] = x;
|
|
61
|
+
|
|
56
62
|
if (x >= left.length && y >= right.length) {
|
|
57
63
|
return true;
|
|
58
64
|
}
|
|
59
65
|
}
|
|
66
|
+
|
|
60
67
|
return false;
|
|
61
68
|
}
|
|
62
69
|
|
|
@@ -66,15 +73,18 @@ function backtrack(left: readonly string[], right: readonly string[], saved: rea
|
|
|
66
73
|
let x = left.length;
|
|
67
74
|
let y = right.length;
|
|
68
75
|
const states = Array.from({ length: Math.min(distance, STRIDE) + 1 }, () => new Int32Array(2 * offset + 1));
|
|
76
|
+
|
|
69
77
|
for (let step = distance; step > 0;) {
|
|
70
78
|
// The states the rounds from the saved one below this step started in, walked again.
|
|
71
79
|
const first = Math.floor((step - 1) / STRIDE) * STRIDE;
|
|
72
80
|
states[0]!.set(saved[first / STRIDE]!);
|
|
81
|
+
|
|
73
82
|
for (let round = first; round < step; round += 1) {
|
|
74
83
|
const next = states[round - first + 1]!;
|
|
75
84
|
next.set(states[round - first]!);
|
|
76
85
|
extend(left, right, next, offset, round);
|
|
77
86
|
}
|
|
87
|
+
|
|
78
88
|
for (; step > first; step -= 1) {
|
|
79
89
|
const state = states[step - first]!;
|
|
80
90
|
const diagonal = x - y;
|
|
@@ -87,6 +97,7 @@ function backtrack(left: readonly string[], right: readonly string[], saved: rea
|
|
|
87
97
|
y = prevY;
|
|
88
98
|
}
|
|
89
99
|
}
|
|
100
|
+
|
|
90
101
|
return moves.reverse();
|
|
91
102
|
}
|
|
92
103
|
|
|
@@ -102,18 +113,23 @@ function walk(left: readonly string[], right: readonly string[], maxWork?: numbe
|
|
|
102
113
|
const saved: Int32Array[] = [];
|
|
103
114
|
// Replay visits each earlier round once; reserve half the budget for backtracking.
|
|
104
115
|
const work = maxWork === undefined ? undefined : { remaining: Math.floor(maxWork / 2) };
|
|
116
|
+
|
|
105
117
|
for (let distance = 0; distance <= limit; distance += 1) {
|
|
106
118
|
if (distance % STRIDE === 0) {
|
|
107
119
|
saved.push(reach.slice());
|
|
108
120
|
}
|
|
121
|
+
|
|
109
122
|
const complete = extend(left, right, reach, offset, distance, work);
|
|
123
|
+
|
|
110
124
|
if (complete === null) {
|
|
111
125
|
return { moves: null, limited: true };
|
|
112
126
|
}
|
|
127
|
+
|
|
113
128
|
if (complete) {
|
|
114
129
|
return { moves: backtrack(left, right, saved, distance, offset), limited: false };
|
|
115
130
|
}
|
|
116
131
|
}
|
|
132
|
+
|
|
117
133
|
return { moves: null, limited: false };
|
|
118
134
|
}
|
|
119
135
|
|
|
@@ -121,11 +137,13 @@ function walk(left: readonly string[], right: readonly string[], maxWork?: numbe
|
|
|
121
137
|
function changesOf(moves: readonly Move[]): Change[] {
|
|
122
138
|
const changes: Change[] = [];
|
|
123
139
|
let current: Change | null = null;
|
|
140
|
+
|
|
124
141
|
for (const move of moves) {
|
|
125
142
|
// A delete moves along the base side and so raises the diagonal; an insert lowers it.
|
|
126
143
|
const deleted = move.x - move.y > move.prevX - move.prevY;
|
|
127
144
|
const editX = deleted ? move.prevX + 1 : move.prevX;
|
|
128
145
|
const editY = deleted ? move.prevY : move.prevY + 1;
|
|
146
|
+
|
|
129
147
|
if (current !== null && current.baseEnd === move.prevX && current.otherEnd === move.prevY) {
|
|
130
148
|
current.baseEnd = editX;
|
|
131
149
|
current.otherEnd = editY;
|
|
@@ -133,17 +151,21 @@ function changesOf(moves: readonly Move[]): Change[] {
|
|
|
133
151
|
if (current !== null) {
|
|
134
152
|
changes.push(current);
|
|
135
153
|
}
|
|
154
|
+
|
|
136
155
|
current = { baseStart: move.prevX, baseEnd: editX, otherStart: move.prevY, otherEnd: editY };
|
|
137
156
|
}
|
|
157
|
+
|
|
138
158
|
// What follows the edit is a run of equal lines, which ends the stretch.
|
|
139
159
|
if (move.x !== editX || move.y !== editY) {
|
|
140
160
|
changes.push(current);
|
|
141
161
|
current = null;
|
|
142
162
|
}
|
|
143
163
|
}
|
|
164
|
+
|
|
144
165
|
if (current !== null) {
|
|
145
166
|
changes.push(current);
|
|
146
167
|
}
|
|
168
|
+
|
|
147
169
|
return changes;
|
|
148
170
|
}
|
|
149
171
|
|
|
@@ -154,20 +176,27 @@ export function diffLines(base: readonly string[], other: readonly string[]): Ch
|
|
|
154
176
|
|
|
155
177
|
export function diffChanges(base: readonly string[], other: readonly string[], maxWork?: number): { changes: Change[]; limited: boolean } {
|
|
156
178
|
let prefix = 0;
|
|
179
|
+
|
|
157
180
|
while (prefix < base.length && prefix < other.length && base[prefix] === other[prefix]) {
|
|
158
181
|
prefix += 1;
|
|
159
182
|
}
|
|
183
|
+
|
|
160
184
|
let suffix = 0;
|
|
185
|
+
|
|
161
186
|
while (suffix < base.length - prefix && suffix < other.length - prefix && base[base.length - 1 - suffix] === other[other.length - 1 - suffix]) {
|
|
162
187
|
suffix += 1;
|
|
163
188
|
}
|
|
189
|
+
|
|
164
190
|
const left = base.slice(prefix, base.length - suffix);
|
|
165
191
|
const right = other.slice(prefix, other.length - suffix);
|
|
192
|
+
|
|
166
193
|
if (left.length === 0 && right.length === 0) {
|
|
167
194
|
return { changes: [], limited: false };
|
|
168
195
|
}
|
|
196
|
+
|
|
169
197
|
const { moves, limited } = left.length === 0 || right.length === 0 ? { moves: null, limited: false } : walk(left, right, maxWork);
|
|
170
198
|
const changes = moves === null ? [{ baseStart: 0, baseEnd: left.length, otherStart: 0, otherEnd: right.length }] : changesOf(moves);
|
|
199
|
+
|
|
171
200
|
return {
|
|
172
201
|
changes: changes.map((change) => ({
|
|
173
202
|
baseStart: change.baseStart + prefix,
|
package/src/patch-of.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { diffChanges, type Change } from './diff.ts';
|
|
2
2
|
import type { Hunk, ParsedPatch, PatchLine } from './patch-parser.ts';
|
|
3
|
-
import { shapeOf } from './text.ts';
|
|
4
|
-
|
|
5
|
-
function fileLines(text: string): string[] {
|
|
6
|
-
if (text === '') {
|
|
7
|
-
return [];
|
|
8
|
-
}
|
|
9
|
-
const lines = text.split('\n');
|
|
10
|
-
if (text.endsWith('\n')) {
|
|
11
|
-
lines.pop();
|
|
12
|
-
}
|
|
13
|
-
return lines;
|
|
14
|
-
}
|
|
3
|
+
import { rawLines, shapeOf } from './text.ts';
|
|
15
4
|
|
|
16
5
|
function keysOf(lines: readonly string[], finalNewline: boolean): string[] {
|
|
17
6
|
return lines.map((line, index) => `${index === lines.length - 1 && !finalNewline ? 'unterminated:' : 'terminated:'}${line}`);
|
|
@@ -24,15 +13,19 @@ function alignChanges(changes: Change[], old: readonly string[], fresh: readonly
|
|
|
24
13
|
const newBound = previous?.otherEnd ?? 0;
|
|
25
14
|
const insert = change.baseStart === change.baseEnd;
|
|
26
15
|
const remove = change.otherStart === change.otherEnd;
|
|
16
|
+
|
|
27
17
|
if (!insert && !remove) {
|
|
28
18
|
continue;
|
|
29
19
|
}
|
|
20
|
+
|
|
30
21
|
// Move a repeated blank from the end of an edit to its start, between adjacent functions.
|
|
31
22
|
while (change.baseStart > oldBound && change.otherStart > newBound) {
|
|
32
23
|
const tail = insert ? fresh[change.otherEnd - 1]! : old[change.baseEnd - 1]!;
|
|
24
|
+
|
|
33
25
|
if (tail.slice(tail.indexOf(':') + 1).trim() !== '' || old[change.baseStart - 1] !== tail || fresh[change.otherStart - 1] !== tail) {
|
|
34
26
|
break;
|
|
35
27
|
}
|
|
28
|
+
|
|
36
29
|
change.baseStart -= 1;
|
|
37
30
|
change.baseEnd -= 1;
|
|
38
31
|
change.otherStart -= 1;
|
|
@@ -49,12 +42,15 @@ export function patchOf(oldText: string, newText: string, context = 3, options?:
|
|
|
49
42
|
if (!Number.isSafeInteger(context) || context < 0) {
|
|
50
43
|
throw new RangeError('Patch context must be a nonnegative safe integer.');
|
|
51
44
|
}
|
|
45
|
+
|
|
52
46
|
const maxWork = options?.maxWork;
|
|
47
|
+
|
|
53
48
|
if (maxWork !== undefined && (!Number.isSafeInteger(maxWork) || maxWork < 0)) {
|
|
54
49
|
throw new RangeError('Patch work budget must be a nonnegative safe integer.');
|
|
55
50
|
}
|
|
56
|
-
|
|
57
|
-
const
|
|
51
|
+
|
|
52
|
+
const old = rawLines(oldText);
|
|
53
|
+
const fresh = rawLines(newText);
|
|
58
54
|
const oldShape = shapeOf(oldText);
|
|
59
55
|
const newShape = shapeOf(newText);
|
|
60
56
|
const oldKeys = keysOf(old, oldShape.finalNewline);
|
|
@@ -63,10 +59,12 @@ export function patchOf(oldText: string, newText: string, context = 3, options?:
|
|
|
63
59
|
alignChanges(changes, oldKeys, newKeys);
|
|
64
60
|
const hunks: Hunk[] = [];
|
|
65
61
|
let group: Change[] = [];
|
|
62
|
+
|
|
66
63
|
const finish = (): void => {
|
|
67
64
|
if (group.length === 0) {
|
|
68
65
|
return;
|
|
69
66
|
}
|
|
67
|
+
|
|
70
68
|
const first = group[0]!;
|
|
71
69
|
const last = group.at(-1)!;
|
|
72
70
|
const before = Math.min(context, first.baseStart, first.otherStart);
|
|
@@ -74,27 +72,35 @@ export function patchOf(oldText: string, newText: string, context = 3, options?:
|
|
|
74
72
|
const oldStart = first.baseStart - before;
|
|
75
73
|
const newStart = first.otherStart - before;
|
|
76
74
|
const lines: PatchLine[] = [];
|
|
75
|
+
|
|
77
76
|
const line = (kind: PatchLine['kind'], at: number): void => {
|
|
78
77
|
const source = kind === 'insert' ? fresh : old;
|
|
79
78
|
const shape = kind === 'insert' ? newShape : oldShape;
|
|
80
79
|
lines.push({ kind, text: source[at]!, ...(at === source.length - 1 && !shape.finalNewline ? { noNewline: true as const } : {}) });
|
|
81
80
|
};
|
|
81
|
+
|
|
82
82
|
let oldAt = oldStart;
|
|
83
|
+
|
|
83
84
|
for (const change of group) {
|
|
84
85
|
for (; oldAt < change.baseStart; oldAt++) {
|
|
85
86
|
line('context', oldAt);
|
|
86
87
|
}
|
|
88
|
+
|
|
87
89
|
for (; oldAt < change.baseEnd; oldAt++) {
|
|
88
90
|
line('delete', oldAt);
|
|
89
91
|
}
|
|
92
|
+
|
|
90
93
|
for (let newAt = change.otherStart; newAt < change.otherEnd; newAt++) {
|
|
91
94
|
line('insert', newAt);
|
|
92
95
|
}
|
|
93
96
|
}
|
|
97
|
+
|
|
94
98
|
for (; oldAt < last.baseEnd + after; oldAt++) {
|
|
95
99
|
line('context', oldAt);
|
|
96
100
|
}
|
|
101
|
+
|
|
97
102
|
const coarse = limited || group.some((change) => change.baseEnd - change.baseStart + change.otherEnd - change.otherStart > 4000);
|
|
103
|
+
|
|
98
104
|
hunks.push({
|
|
99
105
|
oldStart,
|
|
100
106
|
newStart,
|
|
@@ -104,25 +110,34 @@ export function patchOf(oldText: string, newText: string, context = 3, options?:
|
|
|
104
110
|
lines,
|
|
105
111
|
...(coarse ? { coarse: true as const } : {})
|
|
106
112
|
});
|
|
113
|
+
|
|
107
114
|
group = [];
|
|
108
115
|
};
|
|
116
|
+
|
|
109
117
|
for (const change of changes) {
|
|
110
118
|
const previous = group.at(-1);
|
|
119
|
+
|
|
111
120
|
if (previous !== undefined && change.baseStart - previous.baseEnd > 2 * context) {
|
|
112
121
|
finish();
|
|
113
122
|
}
|
|
123
|
+
|
|
114
124
|
group.push(change);
|
|
115
125
|
}
|
|
126
|
+
|
|
116
127
|
finish();
|
|
117
128
|
const parts: string[] = [];
|
|
129
|
+
|
|
118
130
|
for (const hunk of hunks) {
|
|
119
131
|
parts.push(`@@ -${rangeText(hunk.oldStart, hunk.oldCount)} +${rangeText(hunk.newStart, hunk.newCount)} @@`);
|
|
132
|
+
|
|
120
133
|
for (const line of hunk.lines) {
|
|
121
134
|
parts.push(`${line.kind === 'context' ? ' ' : line.kind === 'delete' ? '-' : '+'}${line.text}`);
|
|
135
|
+
|
|
122
136
|
if (line.noNewline) {
|
|
123
137
|
parts.push('\');
|
|
124
138
|
}
|
|
125
139
|
}
|
|
126
140
|
}
|
|
141
|
+
|
|
127
142
|
return { text: parts.length === 0 ? '' : `${parts.join('\n')}\n`, hunks, missing: { old: false, new: false }, partial: false };
|
|
128
143
|
}
|
package/src/patch-parser.ts
CHANGED
|
@@ -35,9 +35,11 @@ export interface HunkHeader {
|
|
|
35
35
|
/* Reads one `@@ -a,b +c,d @@ section` line without its line ending; undefined when it is not a hunk header. */
|
|
36
36
|
export function parseHunkHeader(line: string): HunkHeader | undefined {
|
|
37
37
|
const match = HEADER.exec(line);
|
|
38
|
+
|
|
38
39
|
if (match === null) {
|
|
39
40
|
return undefined;
|
|
40
41
|
}
|
|
42
|
+
|
|
41
43
|
return {
|
|
42
44
|
oldStart: Number(match[1]),
|
|
43
45
|
oldCount: Number(match[2] ?? 1),
|
|
@@ -57,59 +59,78 @@ export function parsePatch(patch: string): ParsedPatch {
|
|
|
57
59
|
let oldEnded = false;
|
|
58
60
|
let newEnded = false;
|
|
59
61
|
const header: string[] = [];
|
|
62
|
+
|
|
60
63
|
const finish = (): void => {
|
|
61
64
|
if (current !== undefined && (oldCount !== current.oldCount || newCount !== current.newCount)) {
|
|
62
65
|
partial = true;
|
|
63
66
|
}
|
|
67
|
+
|
|
64
68
|
current = undefined;
|
|
65
69
|
};
|
|
70
|
+
|
|
66
71
|
for (let index = 0; index < lines.length; index++) {
|
|
67
72
|
const line = lines[index]!;
|
|
68
73
|
const metadata = line.replace(/\r$/, '');
|
|
74
|
+
|
|
69
75
|
if (metadata.startsWith('@@')) {
|
|
70
76
|
finish();
|
|
71
77
|
const match = parseHunkHeader(metadata);
|
|
78
|
+
|
|
72
79
|
if (match === undefined) {
|
|
73
80
|
partial = true;
|
|
81
|
+
|
|
74
82
|
continue;
|
|
75
83
|
}
|
|
84
|
+
|
|
76
85
|
const oldLength = match.oldCount;
|
|
77
86
|
const newLength = match.newCount;
|
|
78
87
|
const oldStart = match.oldStart - (oldLength === 0 ? 0 : 1);
|
|
79
88
|
const newStart = match.newStart - (newLength === 0 ? 0 : 1);
|
|
89
|
+
|
|
80
90
|
if (
|
|
81
91
|
![oldStart, newStart, oldLength, newLength, oldStart + oldLength, newStart + newLength].every(
|
|
82
92
|
(value) => Number.isSafeInteger(value) && value >= 0
|
|
83
93
|
)
|
|
84
94
|
) {
|
|
85
95
|
partial = true;
|
|
96
|
+
|
|
86
97
|
continue;
|
|
87
98
|
}
|
|
99
|
+
|
|
88
100
|
const previous = hunks.at(-1);
|
|
101
|
+
|
|
89
102
|
if (previous !== undefined && (oldStart < previous.oldStart + previous.oldCount || newStart < previous.newStart + previous.newCount)) {
|
|
90
103
|
partial = true;
|
|
91
104
|
}
|
|
105
|
+
|
|
92
106
|
current = { oldStart, oldCount: oldLength, newStart, newCount: newLength, section: match.section, lines: [] };
|
|
93
107
|
hunks.push(current);
|
|
94
108
|
oldCount = 0;
|
|
95
109
|
newCount = 0;
|
|
110
|
+
|
|
96
111
|
continue;
|
|
97
112
|
}
|
|
113
|
+
|
|
98
114
|
if (metadata.startsWith('diff --git ') && hunks.length > 0) {
|
|
99
115
|
finish();
|
|
100
116
|
partial = true;
|
|
117
|
+
|
|
101
118
|
break;
|
|
102
119
|
}
|
|
120
|
+
|
|
103
121
|
if (current === undefined) {
|
|
104
122
|
if (hunks.length === 0) {
|
|
105
123
|
header.push(metadata);
|
|
106
124
|
} else if (line !== '') {
|
|
107
125
|
partial = true;
|
|
108
126
|
}
|
|
127
|
+
|
|
109
128
|
continue;
|
|
110
129
|
}
|
|
130
|
+
|
|
111
131
|
if (metadata === '\') {
|
|
112
132
|
const previous = current.lines.at(-1);
|
|
133
|
+
|
|
113
134
|
if (previous === undefined || previous.noNewline) {
|
|
114
135
|
partial = true;
|
|
115
136
|
} else {
|
|
@@ -117,28 +138,38 @@ export function parsePatch(patch: string): ParsedPatch {
|
|
|
117
138
|
oldEnded ||= previous.kind !== 'insert';
|
|
118
139
|
newEnded ||= previous.kind !== 'delete';
|
|
119
140
|
}
|
|
141
|
+
|
|
120
142
|
continue;
|
|
121
143
|
}
|
|
144
|
+
|
|
122
145
|
// The last empty split field is the patch's terminator, not a source line.
|
|
123
146
|
if (line === '' && index === lines.length - 1) {
|
|
124
147
|
continue;
|
|
125
148
|
}
|
|
149
|
+
|
|
126
150
|
const kind = line.startsWith('-') ? 'delete' : line.startsWith('+') ? 'insert' : line.startsWith(' ') || line === '' ? 'context' : undefined;
|
|
151
|
+
|
|
127
152
|
if (kind === undefined) {
|
|
128
153
|
partial = true;
|
|
154
|
+
|
|
129
155
|
continue;
|
|
130
156
|
}
|
|
157
|
+
|
|
131
158
|
if (kind !== 'insert') {
|
|
132
159
|
partial ||= oldEnded;
|
|
133
160
|
oldCount += 1;
|
|
134
161
|
}
|
|
162
|
+
|
|
135
163
|
if (kind !== 'delete') {
|
|
136
164
|
partial ||= newEnded;
|
|
137
165
|
newCount += 1;
|
|
138
166
|
}
|
|
167
|
+
|
|
139
168
|
current.lines.push({ kind, text: line === '' ? '' : line.slice(1) });
|
|
140
169
|
}
|
|
170
|
+
|
|
141
171
|
finish();
|
|
172
|
+
|
|
142
173
|
return {
|
|
143
174
|
text: patch,
|
|
144
175
|
hunks,
|
package/src/patch.ts
CHANGED
|
@@ -1,56 +1,67 @@
|
|
|
1
1
|
import { parsePatch } from './patch-parser.ts';
|
|
2
|
+
import { lineMatcher } from './text.ts';
|
|
2
3
|
|
|
3
4
|
/* Which side a patch says the file is missing on, read from its header so no line of the file can pass for it. */
|
|
4
5
|
export function missingSides(patch: string): { old: boolean; new: boolean } {
|
|
5
6
|
const start = patch.search(/^@@/m);
|
|
6
7
|
const header = start === -1 ? patch : patch.slice(0, start);
|
|
8
|
+
|
|
7
9
|
return { old: /^--- \/dev\/null$/m.test(header), new: /^\+\+\+ \/dev\/null$/m.test(header) };
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
11
13
|
|
|
12
|
-
/*
|
|
14
|
+
/* A patch cut off before its hunks end fits nothing. */
|
|
13
15
|
export function patchFits(patch: string, oldText: string, newText: string, ignoreWhitespace: boolean): boolean {
|
|
14
16
|
if (parsePatch(patch).partial) {
|
|
15
17
|
return false;
|
|
16
18
|
}
|
|
19
|
+
|
|
17
20
|
const oldLines = oldText.split('\n');
|
|
18
21
|
const newLines = newText.split('\n');
|
|
19
|
-
const same = ignoreWhitespace
|
|
20
|
-
? (shown: string, actual: string | undefined): boolean => actual !== undefined && shown.replace(/\s+/g, '') === actual.replace(/\s+/g, '')
|
|
21
|
-
: (shown: string, actual: string | undefined): boolean => shown === actual;
|
|
22
|
+
const same = lineMatcher(ignoreWhitespace);
|
|
22
23
|
let oldAt = -1;
|
|
23
24
|
let newAt = -1;
|
|
24
25
|
let hunks = 0;
|
|
26
|
+
|
|
25
27
|
for (const line of patch.split('\n')) {
|
|
26
28
|
const header = HUNK_HEADER.exec(line);
|
|
29
|
+
|
|
27
30
|
if (header) {
|
|
28
31
|
hunks += 1;
|
|
29
32
|
oldAt = Number(header[1]) - (header[2] === '0' ? 0 : 1);
|
|
30
33
|
newAt = Number(header[3]) - (header[4] === '0' ? 0 : 1);
|
|
34
|
+
|
|
31
35
|
continue;
|
|
32
36
|
}
|
|
37
|
+
|
|
33
38
|
if (oldAt === -1) {
|
|
34
39
|
continue;
|
|
35
40
|
}
|
|
41
|
+
|
|
36
42
|
const body = line.slice(1);
|
|
43
|
+
|
|
37
44
|
if (line.startsWith(' ')) {
|
|
38
45
|
if (!same(body, oldLines[oldAt]) || !same(body, newLines[newAt])) {
|
|
39
46
|
return false;
|
|
40
47
|
}
|
|
48
|
+
|
|
41
49
|
oldAt += 1;
|
|
42
50
|
newAt += 1;
|
|
43
51
|
} else if (line.startsWith('-')) {
|
|
44
52
|
if (!same(body, oldLines[oldAt])) {
|
|
45
53
|
return false;
|
|
46
54
|
}
|
|
55
|
+
|
|
47
56
|
oldAt += 1;
|
|
48
57
|
} else if (line.startsWith('+')) {
|
|
49
58
|
if (!same(body, newLines[newAt])) {
|
|
50
59
|
return false;
|
|
51
60
|
}
|
|
61
|
+
|
|
52
62
|
newAt += 1;
|
|
53
63
|
}
|
|
54
64
|
}
|
|
65
|
+
|
|
55
66
|
return hunks > 0;
|
|
56
67
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { same, type MergeBlock } from './blocks.ts';
|
|
2
2
|
|
|
3
3
|
export type MergeSide = 'ours' | 'theirs';
|
|
4
4
|
|
|
5
5
|
export function sideLines(block: MergeBlock, side: MergeSide): string[] {
|
|
6
|
-
return
|
|
6
|
+
return [...block[side]];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/* Both sides, one after the other, for the conflict where the two additions both belong. */
|
|
@@ -32,6 +32,7 @@ function runIn(outer: readonly string[], inner: readonly string[]): number {
|
|
|
32
32
|
return start;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
35
36
|
return -1;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -40,11 +41,18 @@ function runIn(outer: readonly string[], inner: readonly string[]): number {
|
|
|
40
41
|
* what `outer` adds is one `inner` deleted, and taking `outer` would put it back.
|
|
41
42
|
*/
|
|
42
43
|
function covers(base: readonly string[], outer: readonly string[], inner: readonly string[]): boolean {
|
|
44
|
+
if (inner.length === 0) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
const start = runIn(outer, inner);
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
if (start < 0) {
|
|
45
51
|
return false;
|
|
46
52
|
}
|
|
53
|
+
|
|
47
54
|
const added = [...outer.slice(0, start), ...outer.slice(start + inner.length)];
|
|
55
|
+
|
|
48
56
|
return added.every((line) => !base.includes(line) || inner.includes(line));
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -57,15 +65,15 @@ export function wandLines(block: MergeBlock): string[] | null {
|
|
|
57
65
|
if (block.kind !== 'conflict') {
|
|
58
66
|
return autoLines(block);
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
if (covers(block.base, block.ours, block.theirs)) {
|
|
68
|
+
|
|
69
|
+
if (same(block.ours, block.theirs) || covers(block.base, block.ours, block.theirs)) {
|
|
64
70
|
return [...block.ours];
|
|
65
71
|
}
|
|
72
|
+
|
|
66
73
|
if (covers(block.base, block.theirs, block.ours)) {
|
|
67
74
|
return [...block.theirs];
|
|
68
75
|
}
|
|
76
|
+
|
|
69
77
|
return null;
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -76,12 +84,15 @@ export function wandLines(block: MergeBlock): string[] | null {
|
|
|
76
84
|
*/
|
|
77
85
|
export function fingerprint(block: MergeBlock): string {
|
|
78
86
|
let hash = 0x811c9dc5;
|
|
87
|
+
|
|
79
88
|
for (const line of [...block.ours, '\u0000', ...block.theirs]) {
|
|
80
89
|
for (let index = 0; index < line.length; index += 1) {
|
|
81
90
|
hash = Math.imul(hash ^ line.charCodeAt(index), 0x01000193);
|
|
82
91
|
}
|
|
92
|
+
|
|
83
93
|
hash = Math.imul(hash ^ 0x0a, 0x01000193);
|
|
84
94
|
}
|
|
95
|
+
|
|
85
96
|
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -106,13 +117,17 @@ export interface MergeDraft {
|
|
|
106
117
|
export function draftOf(blocks: readonly MergeBlock[], answered: ReadonlyMap<number, readonly string[]> = new Map()): MergeDraft {
|
|
107
118
|
const lines: string[] = [];
|
|
108
119
|
const spans: MergeSpan[] = [];
|
|
120
|
+
|
|
109
121
|
for (const [index, block] of blocks.entries()) {
|
|
110
122
|
const from = lines.length;
|
|
123
|
+
|
|
111
124
|
// One push per line: a block of a few hundred thousand lines is more arguments than V8 takes in one call.
|
|
112
125
|
for (const line of answered.get(index) ?? autoLines(block) ?? block.ours) {
|
|
113
126
|
lines.push(line);
|
|
114
127
|
}
|
|
128
|
+
|
|
115
129
|
spans.push({ block: index, kind: block.kind, from, to: lines.length });
|
|
116
130
|
}
|
|
131
|
+
|
|
117
132
|
return { lines, spans };
|
|
118
133
|
}
|
package/src/text.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface TextShape {
|
|
|
10
10
|
/* The line ending a file uses, by the first one in it, and whether it ends on one. */
|
|
11
11
|
export function shapeOf(text: string): TextShape {
|
|
12
12
|
const line = text.indexOf('\n');
|
|
13
|
+
|
|
13
14
|
return {
|
|
14
15
|
eol: line > 0 && text[line - 1] === '\r' ? '\r\n' : '\n',
|
|
15
16
|
finalNewline: text === '' || text.endsWith('\n')
|
|
@@ -21,13 +22,38 @@ export function splitLines(text: string): string[] {
|
|
|
21
22
|
if (text === '') {
|
|
22
23
|
return [];
|
|
23
24
|
}
|
|
25
|
+
|
|
24
26
|
const lines = text.split(/\r\n|\n/);
|
|
27
|
+
|
|
25
28
|
if (lines[lines.length - 1] === '') {
|
|
26
29
|
lines.pop();
|
|
27
30
|
}
|
|
31
|
+
|
|
28
32
|
return lines;
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
/* The lines of a file split on `\n` alone, so a `\r` stays part of its line. */
|
|
36
|
+
export function rawLines(text: string): string[] {
|
|
37
|
+
if (text === '') {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines = text.split('\n');
|
|
42
|
+
|
|
43
|
+
if (text.endsWith('\n')) {
|
|
44
|
+
lines.pop();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return lines;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Git's --ignore-all-space may show either side's spacing, so a shown line matches by its non-space characters. */
|
|
51
|
+
export function lineMatcher(ignoreWhitespace: boolean): (shown: string, actual: string | undefined) => boolean {
|
|
52
|
+
return ignoreWhitespace
|
|
53
|
+
? (shown, actual) => actual !== undefined && shown.replace(/\s+/g, '') === actual.replace(/\s+/g, '')
|
|
54
|
+
: (shown, actual) => shown === actual;
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
export function joinLines(lines: readonly string[], shape: TextShape): string {
|
|
32
58
|
return lines.length === 0 ? '' : lines.join(shape.eol) + (shape.finalNewline ? shape.eol : '');
|
|
33
59
|
}
|
package/src/word-changes.ts
CHANGED
|
@@ -11,16 +11,21 @@ export function wordChanges(oldLine: string, newLine: string): { old: DiffWordRa
|
|
|
11
11
|
if (oldLine.length + newLine.length > 20000) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
+
|
|
14
15
|
segmenter ??= new Intl.Segmenter('en', { granularity: 'word' });
|
|
15
16
|
const before = [...segmenter.segment(oldLine)];
|
|
16
17
|
const after = [...segmenter.segment(newLine)];
|
|
18
|
+
|
|
17
19
|
if (before.length + after.length > 2000) {
|
|
18
20
|
return null;
|
|
19
21
|
}
|
|
22
|
+
|
|
20
23
|
const range = (segments: readonly Intl.SegmentData[], text: string, from: number, to: number): DiffWordRange[] =>
|
|
21
24
|
from === to ? [] : [{ from: segments[from]!.index, to: segments[to]?.index ?? text.length }];
|
|
25
|
+
|
|
22
26
|
const old: DiffWordRange[] = [];
|
|
23
27
|
const fresh: DiffWordRange[] = [];
|
|
28
|
+
|
|
24
29
|
for (const change of diffLines(
|
|
25
30
|
before.map((part) => part.segment),
|
|
26
31
|
after.map((part) => part.segment)
|
|
@@ -28,5 +33,6 @@ export function wordChanges(oldLine: string, newLine: string): { old: DiffWordRa
|
|
|
28
33
|
old.push(...range(before, oldLine, change.baseStart, change.baseEnd));
|
|
29
34
|
fresh.push(...range(after, newLine, change.otherStart, change.otherEnd));
|
|
30
35
|
}
|
|
36
|
+
|
|
31
37
|
return { old, new: fresh };
|
|
32
38
|
}
|