pi-supernova 0.3.1 → 0.4.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/README.md +220 -3
- package/docs/CHANGELOG.md +40 -0
- package/docs/TOKEN_COSTS.md +171 -0
- package/index.js +116 -37
- package/package.json +3 -1
- package/src/bridge/catalog.js +37 -2
- package/src/bridge/host-bridge.js +340 -13
- package/src/bridge/native-tools.js +35 -1
- package/src/config/config.default.json +1 -1
- package/src/config/config.js +19 -0
- package/src/context/evidence.js +119 -5
- package/src/context/fuzzy.js +37 -0
- package/src/context/ledger.js +128 -6
- package/src/context/outline.js +45 -1
- package/src/context/repo-index.js +63 -1
- package/src/context/search.js +45 -0
- package/src/context/snap.js +80 -4
- package/src/context/surface.js +25 -0
- package/src/fs/check.js +50 -0
- package/src/fs/diff.js +16 -0
- package/src/fs/json-read.js +87 -0
- package/src/fs/patch.js +26 -0
- package/src/fs/vfs.js +101 -6
- package/src/fs/workspace.js +36 -0
- package/src/output/bottleneck.js +46 -0
- package/src/output/format.js +117 -17
- package/src/runtime/guest-worker.js +151 -18
- package/src/runtime/parallel.js +33 -0
- package/src/runtime/program-batch.js +112 -0
- package/src/runtime/program-file.js +40 -0
- package/src/runtime/reference.js +25 -0
- package/src/runtime/runtime.js +90 -6
- package/src/shared/decode.js +29 -0
- package/src/ui/omp-frame.js +30 -1
- package/src/ui/render-measure.js +24 -0
- package/src/ui/render.js +96 -2
package/src/fs/check.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// on, so a broken edit is known now instead of after a test run. JSON is checked exactly.
|
|
4
4
|
|
|
5
5
|
const OPEN = { "{": "}", "[": "]", "(": ")" };
|
|
6
|
+
|
|
6
7
|
const CLOSE = new Set(["}", "]", ")"]);
|
|
8
|
+
|
|
7
9
|
const REGEX_PRECEDERS = new Set(["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "+", "-", "*", "%", "<", ">", "~", "^", "return", "typeof", "case", "do", "else", "in", "of"]);
|
|
8
10
|
|
|
9
11
|
function skipString(text, i, quote) {
|
|
@@ -12,9 +14,12 @@ function skipString(text, i, quote) {
|
|
|
12
14
|
j++;
|
|
13
15
|
continue;
|
|
14
16
|
}
|
|
17
|
+
|
|
15
18
|
if (text[j] === quote) return j + 1;
|
|
19
|
+
|
|
16
20
|
if (quote !== "`" && text[j] === "\n") return -1;
|
|
17
21
|
}
|
|
22
|
+
|
|
18
23
|
return -1;
|
|
19
24
|
}
|
|
20
25
|
|
|
@@ -24,13 +29,17 @@ function skipTemplate(text, i, stack) {
|
|
|
24
29
|
j++;
|
|
25
30
|
continue;
|
|
26
31
|
}
|
|
32
|
+
|
|
27
33
|
if (text[j] === "`") return j + 1;
|
|
34
|
+
|
|
28
35
|
if (text[j] === "$" && text[j + 1] === "{") {
|
|
29
36
|
const end = balancedEnd(text, j + 1, stack);
|
|
37
|
+
|
|
30
38
|
if (end < 0) return -1;
|
|
31
39
|
j = end - 1; // loop increment lands on the char after "}"
|
|
32
40
|
}
|
|
33
41
|
}
|
|
42
|
+
|
|
34
43
|
return -1;
|
|
35
44
|
}
|
|
36
45
|
|
|
@@ -38,67 +47,90 @@ function skipTemplate(text, i, stack) {
|
|
|
38
47
|
function balancedEnd(text, i, stack) {
|
|
39
48
|
const depth = stack.length;
|
|
40
49
|
const r = scan(text, i, stack, depth);
|
|
50
|
+
|
|
41
51
|
return r.error ? -1 : r.end;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
function skipComment(text, i) {
|
|
45
55
|
if (text[i + 1] === "/") {
|
|
46
56
|
const nl = text.indexOf("\n", i);
|
|
57
|
+
|
|
47
58
|
return nl < 0 ? text.length : nl;
|
|
48
59
|
}
|
|
60
|
+
|
|
49
61
|
const end = text.indexOf("*/", i + 2);
|
|
62
|
+
|
|
50
63
|
return end < 0 ? text.length : end + 2;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
function skipRegex(text, i) {
|
|
54
67
|
let inClass = false;
|
|
68
|
+
|
|
55
69
|
for (let j = i + 1; j < text.length; j++) {
|
|
56
70
|
const c = text[j];
|
|
71
|
+
|
|
57
72
|
if (c === "\\") {
|
|
58
73
|
j++;
|
|
59
74
|
continue;
|
|
60
75
|
}
|
|
76
|
+
|
|
61
77
|
if (c === "\n") return -1;
|
|
78
|
+
|
|
62
79
|
if (c === "[") inClass = true;
|
|
63
80
|
else if (c === "]") inClass = false;
|
|
64
81
|
else if (c === "/" && !inClass) return j + 1;
|
|
65
82
|
}
|
|
83
|
+
|
|
66
84
|
return -1;
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
function lineOf(text, i) {
|
|
70
88
|
let n = 1;
|
|
89
|
+
|
|
71
90
|
for (let j = 0; j < i && j < text.length; j++) if (text[j] === "\n") n++;
|
|
91
|
+
|
|
72
92
|
return n;
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
const IDENT_START = /[A-Za-z_$]/;
|
|
96
|
+
|
|
76
97
|
const IDENT_PART = /[\w$]/;
|
|
77
98
|
|
|
78
99
|
function readIdentifier(text, i) {
|
|
79
100
|
let j = i + 1;
|
|
101
|
+
|
|
80
102
|
while (j < text.length && IDENT_PART.test(text[j])) j++;
|
|
103
|
+
|
|
81
104
|
return j;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
function consumeQuoted(text, i, stack) {
|
|
85
108
|
const c = text[i];
|
|
109
|
+
|
|
86
110
|
if (c === "`") {
|
|
87
111
|
const end = skipTemplate(text, i, stack);
|
|
112
|
+
|
|
88
113
|
return end < 0 ? { error: "unterminated template literal", at: i } : { end, prev: "value" };
|
|
89
114
|
}
|
|
115
|
+
|
|
90
116
|
const end = skipString(text, i, c);
|
|
117
|
+
|
|
91
118
|
return end < 0 ? { error: "unterminated string", at: i } : { end, prev: "value" };
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
/** Try to consume a comment, string, template, or regex at i. Returns { end, prev } | { error, at } | null. */
|
|
95
122
|
function consumeLiteral(text, i, stack, prev) {
|
|
96
123
|
const c = text[i];
|
|
124
|
+
|
|
97
125
|
if (c === '"' || c === "'" || c === "`") return consumeQuoted(text, i, stack);
|
|
126
|
+
|
|
98
127
|
if (c !== "/") return null;
|
|
128
|
+
|
|
99
129
|
if (text[i + 1] === "/" || text[i + 1] === "*") return { end: skipComment(text, i), prev };
|
|
130
|
+
|
|
100
131
|
if (prev !== "" && !REGEX_PRECEDERS.has(prev)) return null;
|
|
101
132
|
const end = skipRegex(text, i);
|
|
133
|
+
|
|
102
134
|
return end > 0 ? { end, prev: "value" } : null;
|
|
103
135
|
}
|
|
104
136
|
|
|
@@ -106,12 +138,17 @@ function consumeLiteral(text, i, stack, prev) {
|
|
|
106
138
|
function bracket(c, i, stack, stopDepth) {
|
|
107
139
|
if (OPEN[c]) {
|
|
108
140
|
stack.push({ c, at: i });
|
|
141
|
+
|
|
109
142
|
return null;
|
|
110
143
|
}
|
|
144
|
+
|
|
111
145
|
if (!CLOSE.has(c)) return null;
|
|
112
146
|
const top = stack.pop();
|
|
147
|
+
|
|
113
148
|
if (!top || OPEN[top.c] !== c) return { error: "unexpected '" + c + "'", at: i };
|
|
149
|
+
|
|
114
150
|
if (stopDepth !== undefined && stack.length <= stopDepth) return { end: i + 1 };
|
|
151
|
+
|
|
115
152
|
return null;
|
|
116
153
|
}
|
|
117
154
|
|
|
@@ -119,26 +156,33 @@ function bracket(c, i, stack, stopDepth) {
|
|
|
119
156
|
function scan(text, start, stack, stopDepth) {
|
|
120
157
|
let i = start;
|
|
121
158
|
let prev = "";
|
|
159
|
+
|
|
122
160
|
while (i < text.length) {
|
|
123
161
|
const c = text[i];
|
|
124
162
|
const literal = consumeLiteral(text, i, stack, prev);
|
|
163
|
+
|
|
125
164
|
if (literal) {
|
|
126
165
|
if (literal.error) return literal;
|
|
127
166
|
i = literal.end;
|
|
128
167
|
prev = literal.prev;
|
|
129
168
|
continue;
|
|
130
169
|
}
|
|
170
|
+
|
|
131
171
|
if (IDENT_START.test(c)) {
|
|
132
172
|
const j = readIdentifier(text, i);
|
|
133
173
|
prev = text.slice(i, j);
|
|
134
174
|
i = j;
|
|
135
175
|
continue;
|
|
136
176
|
}
|
|
177
|
+
|
|
137
178
|
const outcome = bracket(c, i, stack, stopDepth);
|
|
179
|
+
|
|
138
180
|
if (outcome) return outcome;
|
|
181
|
+
|
|
139
182
|
if (!/\s/.test(c)) prev = c;
|
|
140
183
|
i++;
|
|
141
184
|
}
|
|
185
|
+
|
|
142
186
|
return { end: i };
|
|
143
187
|
}
|
|
144
188
|
|
|
@@ -149,18 +193,24 @@ export function quickCheck(text, ext) {
|
|
|
149
193
|
if (ext === ".json") {
|
|
150
194
|
try {
|
|
151
195
|
JSON.parse(text);
|
|
196
|
+
|
|
152
197
|
return { ok: true, kind: "json" };
|
|
153
198
|
} catch (err) {
|
|
154
199
|
return { ok: false, kind: "json", message: String(err.message).replace(/^JSON\.parse: /, "") };
|
|
155
200
|
}
|
|
156
201
|
}
|
|
202
|
+
|
|
157
203
|
if (!CODE_EXT.has(ext)) return null;
|
|
158
204
|
const stack = [];
|
|
159
205
|
const r = scan(text, 0, stack);
|
|
206
|
+
|
|
160
207
|
if (r.error) return { ok: false, kind: "balance", message: r.error + " at line " + lineOf(text, r.at) };
|
|
208
|
+
|
|
161
209
|
if (stack.length) {
|
|
162
210
|
const top = stack[stack.length - 1];
|
|
211
|
+
|
|
163
212
|
return { ok: false, kind: "balance", message: "unclosed '" + top.c + "' opened at line " + lineOf(text, top.at) };
|
|
164
213
|
}
|
|
214
|
+
|
|
165
215
|
return { ok: true, kind: "balance" };
|
|
166
216
|
}
|
package/src/fs/diff.js
CHANGED
|
@@ -10,6 +10,7 @@ export function buildEditDiff(filePath, originalText, oldText, newText) {
|
|
|
10
10
|
const newLines = contentLines(newText);
|
|
11
11
|
|
|
12
12
|
const lines = [];
|
|
13
|
+
|
|
13
14
|
if (startLine > 1 && fileLines.length >= startLine - 1) {
|
|
14
15
|
lines.push({ type: "context", lineNum: startLine - 1, text: fileLines[startLine - 2] });
|
|
15
16
|
}
|
|
@@ -17,11 +18,13 @@ export function buildEditDiff(filePath, originalText, oldText, newText) {
|
|
|
17
18
|
for (let i = 0; i < oldLines.length; i++) {
|
|
18
19
|
lines.push({ type: "remove", lineNum: startLine + i, text: oldLines[i] });
|
|
19
20
|
}
|
|
21
|
+
|
|
20
22
|
for (let i = 0; i < newLines.length; i++) {
|
|
21
23
|
lines.push({ type: "add", lineNum: startLine + i, text: newLines[i] });
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const afterSourceLine = startLine + oldLines.length;
|
|
27
|
+
|
|
25
28
|
if (fileLines.length >= afterSourceLine) {
|
|
26
29
|
lines.push({ type: "context", lineNum: startLine + newLines.length, text: fileLines[afterSourceLine - 1] });
|
|
27
30
|
}
|
|
@@ -39,16 +42,20 @@ export function buildMultiEditDiff(filePath, originalText, replacements) {
|
|
|
39
42
|
const parts = replacements.map(({ oldText, newText }) =>
|
|
40
43
|
buildEditDiff(filePath, originalText, oldText, newText),
|
|
41
44
|
);
|
|
45
|
+
|
|
42
46
|
const lines = [];
|
|
43
47
|
let shift = 0;
|
|
48
|
+
|
|
44
49
|
for (const [index, part] of parts.entries()) {
|
|
45
50
|
for (const line of part.lines) {
|
|
46
51
|
if (line.type === "context") continue;
|
|
47
52
|
lines.push(line.type === "add" ? { ...line, lineNum: line.lineNum + shift }
|
|
48
53
|
: { ...line, newLineNum: Math.max(1, line.lineNum + shift) });
|
|
49
54
|
}
|
|
55
|
+
|
|
50
56
|
shift += replacements[index].newText.split("\n").length - replacements[index].oldText.split("\n").length;
|
|
51
57
|
}
|
|
58
|
+
|
|
52
59
|
return {
|
|
53
60
|
path: filePath,
|
|
54
61
|
op: "edit",
|
|
@@ -75,6 +82,7 @@ export function buildPatchDiff(filePath, patchText) {
|
|
|
75
82
|
|
|
76
83
|
for (const patchLine of patchLines) {
|
|
77
84
|
const headerMatch = (/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?/).exec(patchLine);
|
|
85
|
+
|
|
78
86
|
if (headerMatch) {
|
|
79
87
|
// A zero-length range names the line before the insertion/deletion point.
|
|
80
88
|
oldLineNum = Number(headerMatch[1]) + Number(headerMatch[2] === "0");
|
|
@@ -82,9 +90,12 @@ export function buildPatchDiff(filePath, patchText) {
|
|
|
82
90
|
inHunk = true;
|
|
83
91
|
continue;
|
|
84
92
|
}
|
|
93
|
+
|
|
85
94
|
if (!inHunk || patchLine.startsWith("\\")) continue;
|
|
86
95
|
const kind = classifyPatchLine(patchLine);
|
|
96
|
+
|
|
87
97
|
if (!kind) continue;
|
|
98
|
+
|
|
88
99
|
if (kind === "remove") {
|
|
89
100
|
removed += 1;
|
|
90
101
|
lines.push({ type: "remove", lineNum: oldLineNum, newLineNum, text: patchLine.slice(1) });
|
|
@@ -106,7 +117,9 @@ export function buildPatchDiff(filePath, patchText) {
|
|
|
106
117
|
function contentLines(text) {
|
|
107
118
|
if (!isString(text) || text.length === 0) return [];
|
|
108
119
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
120
|
+
|
|
109
121
|
if (lines.at(-1) === "") lines.pop();
|
|
122
|
+
|
|
110
123
|
return lines;
|
|
111
124
|
}
|
|
112
125
|
|
|
@@ -115,12 +128,15 @@ export function buildWriteDiff(filePath, previousText, newText) {
|
|
|
115
128
|
const oldLines = contentLines(previousText);
|
|
116
129
|
const maxStoredLines = 64;
|
|
117
130
|
const lines = [];
|
|
131
|
+
|
|
118
132
|
for (let i = 0; i < oldLines.length && lines.length < maxStoredLines; i++) {
|
|
119
133
|
lines.push({ type: "remove", lineNum: i + 1, text: oldLines[i] });
|
|
120
134
|
}
|
|
135
|
+
|
|
121
136
|
for (let i = 0; i < newLines.length && lines.length < maxStoredLines; i++) {
|
|
122
137
|
lines.push({ type: "add", lineNum: i + 1, text: newLines[i] });
|
|
123
138
|
}
|
|
139
|
+
|
|
124
140
|
return {
|
|
125
141
|
path: filePath,
|
|
126
142
|
op: "write",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isString, isObject } from "../shared/decode.js";
|
|
2
|
+
|
|
3
|
+
export const MAX_JSON_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
const SELECTOR_HELP = 'JSON selector supports .field, .nested[0], .items[0:3], .["quoted.key"], or . (whole value); not full jq';
|
|
6
|
+
|
|
7
|
+
/** Parse a small, non-evaluating selector language. No dynamic code or prototype lookup. */
|
|
8
|
+
function parseSelector(selector) {
|
|
9
|
+
if (!isString(selector) || !selector.startsWith(".") || selector.length > 2048) throw new Error(SELECTOR_HELP);
|
|
10
|
+
let rest = selector.slice(1);
|
|
11
|
+
const steps = [];
|
|
12
|
+
let first = true;
|
|
13
|
+
|
|
14
|
+
while (rest) {
|
|
15
|
+
let match;
|
|
16
|
+
|
|
17
|
+
if ((match = (first ? /^([A-Za-z_$][\w$]*)/ : /^\.([A-Za-z_$][\w$]*)/).exec(rest))) {
|
|
18
|
+
steps.push({ key: match[1] });
|
|
19
|
+
} else if ((match = /^\[("(?:[^"\\]|\\.)*")\]/.exec(rest))) {
|
|
20
|
+
try { steps.push({ key: JSON.parse(match[1]) }); } catch { throw new Error(SELECTOR_HELP); }
|
|
21
|
+
} else if ((match = /^\[(\d+)(?::(\d+))?\]/.exec(rest))) {
|
|
22
|
+
const start = Number(match[1]), end = match[2] === undefined ? undefined : Number(match[2]);
|
|
23
|
+
|
|
24
|
+
if (!Number.isSafeInteger(start) || (end !== undefined && (!Number.isSafeInteger(end) || end < start))) throw new Error(SELECTOR_HELP);
|
|
25
|
+
steps.push(end === undefined ? { index: start } : { start, end });
|
|
26
|
+
} else throw new Error(SELECTOR_HELP);
|
|
27
|
+
rest = rest.slice(match[0].length);
|
|
28
|
+
first = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return steps;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function jsonProjector(json) {
|
|
35
|
+
const many = Array.isArray(json);
|
|
36
|
+
const selectors = many ? json : [json === true ? "." : json];
|
|
37
|
+
|
|
38
|
+
if (!selectors.length || selectors.length > 64) throw new Error("JSON selector list requires 1 to 64 selectors");
|
|
39
|
+
const plans = Array.from(selectors, parseSelector);
|
|
40
|
+
|
|
41
|
+
// Yield one selection at a time so the caller can budget it before the next
|
|
42
|
+
// slice allocation. Eagerly mapping 64 large slices can exhaust the host heap.
|
|
43
|
+
return function* (root) {
|
|
44
|
+
for (const steps of plans) yield steps.reduce((value, step) => {
|
|
45
|
+
if (step.key !== undefined) {
|
|
46
|
+
if (!isObject(value) || !Object.hasOwn(value, step.key)) throw new Error("JSON field not found: " + JSON.stringify(step.key));
|
|
47
|
+
|
|
48
|
+
return value[step.key];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!Array.isArray(value)) throw new Error("JSON index requires an array");
|
|
52
|
+
|
|
53
|
+
if (step.index !== undefined) {
|
|
54
|
+
if (step.index >= value.length) throw new Error("JSON index out of range: " + step.index);
|
|
55
|
+
|
|
56
|
+
return value[step.index];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return value.slice(step.start, step.end);
|
|
60
|
+
}, root);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function sessionJsonArgs(args) {
|
|
65
|
+
if (!isString(args.path) || !/^(agent|artifact):\/\//i.test(args.path) || !args.path.includes("?")) return args;
|
|
66
|
+
const [uri, query] = args.path.split("?");
|
|
67
|
+
const params = new URLSearchParams(query);
|
|
68
|
+
|
|
69
|
+
if (args.path.includes("#") || args.path.split("?").length !== 2 || params.size !== 1 || !params.has("q") || args.json !== undefined) {
|
|
70
|
+
throw new Error("session resource supports only one ?q=<JSON selector>; do not combine it with json");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const json = params.get("q");
|
|
74
|
+
jsonProjector(json); // Reject invalid selectors before artifact lookup.
|
|
75
|
+
|
|
76
|
+
return { ...args, path: uri, json };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateJsonRead(args) {
|
|
80
|
+
if (args.json === undefined) return;
|
|
81
|
+
|
|
82
|
+
if (["offset", "limit", "about", "query", "outline", "evidence", "resolve", "complete"].some(key => args[key] !== undefined)) {
|
|
83
|
+
throw new Error("JSON reads cannot combine json with line windows, source views, or complete; select fields after parsing the whole document");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
jsonProjector(args.json);
|
|
87
|
+
}
|
package/src/fs/patch.js
CHANGED
|
@@ -2,7 +2,9 @@ import { isString } from "../shared/decode.js";
|
|
|
2
2
|
|
|
3
3
|
function parseHunkHeader(line) {
|
|
4
4
|
const match = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/.exec(line);
|
|
5
|
+
|
|
5
6
|
if (!match) return null;
|
|
7
|
+
|
|
6
8
|
return { oldStart: Number(match[1]), oldLength: match[2] === undefined ? 1 : Number(match[2]),
|
|
7
9
|
newStart: Number(match[3]), newLength: match[4] === undefined ? 1 : Number(match[4]), lines: [], noNewline: [] };
|
|
8
10
|
}
|
|
@@ -12,8 +14,10 @@ export function parsePatchHunks(patchText) {
|
|
|
12
14
|
let current;
|
|
13
15
|
let oldCount = 0;
|
|
14
16
|
let newCount = 0;
|
|
17
|
+
|
|
15
18
|
for (const line of patchText.split("\n")) {
|
|
16
19
|
const header = parseHunkHeader(line);
|
|
20
|
+
|
|
17
21
|
if (header) {
|
|
18
22
|
current = header;
|
|
19
23
|
hunks.push(current);
|
|
@@ -26,12 +30,17 @@ export function parsePatchHunks(patchText) {
|
|
|
26
30
|
if (oldCount === current.oldLength && newCount === current.newLength && /^--- |^\+\+\+ /.test(line)) {
|
|
27
31
|
throw new Error("apply_patch accepts one file at a time");
|
|
28
32
|
}
|
|
33
|
+
|
|
29
34
|
current.lines.push(line);
|
|
35
|
+
|
|
30
36
|
if (line[0] !== "+") oldCount++;
|
|
37
|
+
|
|
31
38
|
if (line[0] !== "-") newCount++;
|
|
32
39
|
}
|
|
33
40
|
}
|
|
41
|
+
|
|
34
42
|
if (!hunks.length) throw new Error("no valid patch hunks found (expected @@ -old,len +new,len @@)");
|
|
43
|
+
|
|
35
44
|
return hunks;
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -39,10 +48,13 @@ function splitFile(text) {
|
|
|
39
48
|
if (!text) return [];
|
|
40
49
|
const chunks = text.split("\n");
|
|
41
50
|
const trailing = chunks.at(-1) === "";
|
|
51
|
+
|
|
42
52
|
if (trailing) chunks.pop();
|
|
53
|
+
|
|
43
54
|
return chunks.map((chunk, index) => {
|
|
44
55
|
const newline = index < chunks.length - 1 || trailing;
|
|
45
56
|
const crlf = newline && chunk.endsWith("\r");
|
|
57
|
+
|
|
46
58
|
return { text: crlf ? chunk.slice(0, -1) : chunk, ending: newline ? (crlf ? "\r\n" : "\n") : "" };
|
|
47
59
|
});
|
|
48
60
|
}
|
|
@@ -50,12 +62,17 @@ function splitFile(text) {
|
|
|
50
62
|
function findHunkMatch(fileLines, expectedOld, nominal) {
|
|
51
63
|
const matchAt = index => index >= 0 && index + expectedOld.length <= fileLines.length
|
|
52
64
|
&& expectedOld.every((line, i) => fileLines[index + i].text === line);
|
|
65
|
+
|
|
53
66
|
if (matchAt(nominal)) return nominal;
|
|
67
|
+
|
|
54
68
|
if (!expectedOld.length) return -1;
|
|
69
|
+
|
|
55
70
|
for (let delta = 1; delta <= Math.max(fileLines.length, 100); delta++) {
|
|
56
71
|
if (matchAt(nominal + delta)) return nominal + delta;
|
|
72
|
+
|
|
57
73
|
if (matchAt(nominal - delta)) return nominal - delta;
|
|
58
74
|
}
|
|
75
|
+
|
|
59
76
|
return -1;
|
|
60
77
|
}
|
|
61
78
|
|
|
@@ -66,31 +83,40 @@ export function applyPatchToText(originalText, patchText) {
|
|
|
66
83
|
const ending = fileLines.find(line => line.ending)?.ending ?? "\n";
|
|
67
84
|
let offsetShift = 0;
|
|
68
85
|
let relocationShift = 0;
|
|
86
|
+
|
|
69
87
|
for (let h = 0; h < hunks.length; h++) {
|
|
70
88
|
const hunk = hunks[h];
|
|
71
89
|
const textOf = line => line.slice(1).replace(/\r$/, "");
|
|
72
90
|
const expectedOld = hunk.lines.filter(line => line[0] !== "+").map(textOf);
|
|
73
91
|
const newCount = hunk.lines.filter(line => line[0] !== "-").length;
|
|
92
|
+
|
|
74
93
|
if (expectedOld.length !== hunk.oldLength || newCount !== hunk.newLength) throw new Error("patch hunk " + (h + 1) + " length does not match its header");
|
|
75
94
|
// The new coordinate also handles BSD diff's -1,0 header at file start.
|
|
76
95
|
const nominal = hunk.oldLength === 0 ? hunk.newStart - 1 + relocationShift : hunk.oldStart - 1 + offsetShift;
|
|
77
96
|
const matchIndex = findHunkMatch(fileLines, expectedOld, nominal);
|
|
97
|
+
|
|
78
98
|
if (matchIndex < 0) throw new Error("patch hunk " + (h + 1) + " rejected at line " + hunk.oldStart + ": context did not match");
|
|
79
99
|
const replacement = [];
|
|
80
100
|
let oldIndex = matchIndex;
|
|
101
|
+
|
|
81
102
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
82
103
|
const line = hunk.lines[i];
|
|
104
|
+
|
|
83
105
|
if (line[0] === "+") {
|
|
84
106
|
replacement.push({ text: textOf(line), ending: hunk.noNewline.includes(i) ? "" : line.endsWith("\r") ? "\r\n" : ending });
|
|
85
107
|
} else {
|
|
86
108
|
const original = fileLines[oldIndex++];
|
|
109
|
+
|
|
87
110
|
if (hunk.noNewline.includes(i) && original.ending) throw new Error("patch newline marker does not match the file");
|
|
111
|
+
|
|
88
112
|
if (line[0] === " ") replacement.push(original);
|
|
89
113
|
}
|
|
90
114
|
}
|
|
115
|
+
|
|
91
116
|
fileLines.splice(matchIndex, expectedOld.length, ...replacement);
|
|
92
117
|
relocationShift += matchIndex - nominal;
|
|
93
118
|
offsetShift += matchIndex - nominal + replacement.length - expectedOld.length;
|
|
94
119
|
}
|
|
120
|
+
|
|
95
121
|
return { resultText: fileLines.map(line => line.text + line.ending).join(""), hunkCount: hunks.length };
|
|
96
122
|
}
|