@nudojs/service 0.3.0 → 0.3.2

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.
Files changed (72) hide show
  1. package/dist/index.d.ts +18 -3
  2. package/dist/index.js +97 -59
  3. package/package.json +16 -6
  4. package/CHANGELOG.md +0 -68
  5. package/src/__tests__/abs-bindings.test.ts +0 -53
  6. package/src/__tests__/abs-call-records.test.ts +0 -33
  7. package/src/__tests__/abs-calls.test.ts +0 -59
  8. package/src/__tests__/abs-module-cache.test.ts +0 -121
  9. package/src/__tests__/abs-modules-graph.test.ts +0 -100
  10. package/src/__tests__/abs-typevalue-diff.test.ts +0 -111
  11. package/src/__tests__/analyzer.test.ts +0 -880
  12. package/src/__tests__/as-replace.test.ts +0 -165
  13. package/src/__tests__/bpath-arg-provenance.test.ts +0 -104
  14. package/src/__tests__/bpath-as.test.ts +0 -54
  15. package/src/__tests__/bpath-cache-key.test.ts +0 -33
  16. package/src/__tests__/bpath-calls.test.ts +0 -55
  17. package/src/__tests__/bpath-diagnostics.test.ts +0 -78
  18. package/src/__tests__/bpath-env-modules.test.ts +0 -30
  19. package/src/__tests__/bpath-env.test.ts +0 -80
  20. package/src/__tests__/bpath-hosted-skip.test.ts +0 -71
  21. package/src/__tests__/bpath-import-default-ns.test.ts +0 -106
  22. package/src/__tests__/bpath-method-dedupe.test.ts +0 -65
  23. package/src/__tests__/bpath-method-diag.test.ts +0 -45
  24. package/src/__tests__/bpath-module-diags.test.ts +0 -101
  25. package/src/__tests__/bpath-path-env.test.ts +0 -46
  26. package/src/__tests__/bpath-reexport.test.ts +0 -131
  27. package/src/__tests__/bpath-replace.test.ts +0 -52
  28. package/src/__tests__/bpath-require.test.ts +0 -66
  29. package/src/__tests__/bpath-run.test.ts +0 -109
  30. package/src/__tests__/case-emitter.test.ts +0 -636
  31. package/src/__tests__/cjs-require.test.ts +0 -54
  32. package/src/__tests__/diagnostics.test.ts +0 -60
  33. package/src/__tests__/dts-generator.test.ts +0 -382
  34. package/src/__tests__/fn-cache-evict.test.ts +0 -56
  35. package/src/__tests__/fn-sig-impl.test.ts +0 -247
  36. package/src/__tests__/guard-generator.test.ts +0 -29
  37. package/src/__tests__/harvest-auto.test.ts +0 -43
  38. package/src/__tests__/harvest-node.test.ts +0 -24
  39. package/src/__tests__/harvest-package.test.ts +0 -38
  40. package/src/__tests__/harvest-to-abs.test.ts +0 -46
  41. package/src/__tests__/hover-intension.test.ts +0 -39
  42. package/src/__tests__/infer-json.test.ts +0 -35
  43. package/src/__tests__/integration.test.ts +0 -421
  44. package/src/__tests__/m3-intension.test.ts +0 -28
  45. package/src/__tests__/mock-seed-fingerprint.test.ts +0 -64
  46. package/src/__tests__/schema-generator.test.ts +0 -38
  47. package/src/__tests__/session-cache-evict.test.ts +0 -90
  48. package/src/__tests__/static-imports.test.ts +0 -44
  49. package/src/abs-modules-graph.ts +0 -387
  50. package/src/analysis-file-cache.ts +0 -51
  51. package/src/analyzer.ts +0 -3133
  52. package/src/bpath-diagnostics.ts +0 -283
  53. package/src/bpath-run.ts +0 -409
  54. package/src/case-emitter.ts +0 -477
  55. package/src/dts-generator.ts +0 -291
  56. package/src/env-to-abs.ts +0 -57
  57. package/src/fn-analysis-cache.ts +0 -119
  58. package/src/guard-generator.ts +0 -57
  59. package/src/harvest-auto.ts +0 -86
  60. package/src/harvest-node.ts +0 -48
  61. package/src/harvest-package.ts +0 -204
  62. package/src/harvest-to-abs.ts +0 -129
  63. package/src/index.ts +0 -150
  64. package/src/infer-json.ts +0 -137
  65. package/src/load-module.ts +0 -34
  66. package/src/mock-abs.ts +0 -275
  67. package/src/schema-generator.ts +0 -43
  68. package/src/semantic-tokens.ts +0 -216
  69. package/src/session-cache.ts +0 -50
  70. package/src/static-imports.ts +0 -241
  71. package/src/target-path.ts +0 -17
  72. package/tsconfig.json +0 -13
@@ -1,477 +0,0 @@
1
- import { type TypeValue } from "@nudojs/core";
2
- import { parse, extractDirectives } from "@nudojs/parser";
3
- import type { CaseDirective } from "@nudojs/parser";
4
- import type { AnalysisResult } from "./analyzer.ts";
5
-
6
- /**
7
- * 调用点固化(case emission):把分析产出的合成 case(call@L* / call@symbolic)
8
- * 序列化为 `@nudo:case` 指令注释文本、从源码剥离已生成指令、把指令插回源码。
9
- *
10
- * 指令文法权威是 parser 的 parseTypeValueExpr;它对字符串字面量做的是
11
- * 原样 slice(不反转义),因此这里的"转义"策略是:选择一种引号包裹使值
12
- * 原样保留(含 `"` 的值用单引号包裹,反之亦然),无法原样保留的值一律
13
- * 返回 null(见 serializeStringLiteral)。
14
- */
15
-
16
- /** 合成 case 的名字前缀;带此前缀的 @nudo:case 视为本模块生成物 */
17
- const GENERATED_PREFIX = "call@";
18
-
19
- /** 行首形如 ` * @nudo:case "call@…" (` 的指令行(整行都是指令内容) */
20
- const GENERATED_CASE_LINE_REGEX = /^\s*\*?\s*@nudo:case\s+"(call@[^"]*)"/;
21
-
22
- /** 合法裸写的对象键:JS 标识符 */
23
- const IDENT_KEY_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
24
-
25
- /** parseTypeValueExpr 能吃回去的数字字面量(科学计数法等不收) */
26
- const NUMBER_LITERAL_REGEX = /^-?\d+(\.\d+)?$/;
27
-
28
- /** 字符串值中会破坏指令行结构的字符:参数逗号切分 / 括号配对(冒号在值位置安全,键位置才禁) */
29
- const UNSAFE_STRING_CHARS = /[,()[\]{}]/;
30
-
31
- /** 对象键的额外禁用字符:键内冒号会让解析侧的 findTopLevelColon 错位 */
32
- const UNSAFE_KEY_CHARS = /[,()[\]{}:]/;
33
-
34
- /** 控制字符(含换行):指令必须单行 */
35
- const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
36
-
37
- /**
38
- * 字符串字面量 → 可被 parseTypeValueExpr 原样解析回去的带引号文本。
39
- * 两种引号并存、含逗号/括号/冒号等结构字符、控制字符、块注释终止序列
40
- * (会提前结束 JSDoc 块)时无法安全表达,返回 null。
41
- */
42
- function serializeStringLiteral(value: string): string | null {
43
- if (CONTROL_CHARS.test(value)) return null;
44
- if (value.includes("*/")) return null;
45
- if (UNSAFE_STRING_CHARS.test(value)) return null;
46
- const hasDbl = value.includes('"');
47
- const hasSgl = value.includes("'");
48
- if (hasDbl && hasSgl) return null;
49
- return hasDbl ? `'${value}'` : `"${value}"`;
50
- }
51
-
52
- /** 对象键 → 裸标识符或双引号包裹;无法安全表达返回 null */
53
- function serializeObjectKey(key: string): string | null {
54
- if (IDENT_KEY_REGEX.test(key)) return key;
55
- if (CONTROL_CHARS.test(key)) return null;
56
- if (key.includes('"')) return null; // 双引号包裹承载不了自带双引号的键
57
- if (UNSAFE_KEY_CHARS.test(key) || key.includes("*/")) return null;
58
- // 解析侧 strip 正则会吃掉首尾引号字符,键自身以引号开头/结尾时会错位
59
- if (/^['"]|['"]$/.test(key)) return null;
60
- return `"${key}"`;
61
- }
62
-
63
- /** 单个 TypeValue → parseTypeValueExpr 可解析回去的表达式文本;不可表达返回 null */
64
- export function serializeCaseArg(tv: TypeValue): string | null {
65
- switch (tv.kind) {
66
- case "primitive":
67
- // bigint / symbol 不在指令文法内
68
- if (tv.type === "number" || tv.type === "string" || tv.type === "boolean") {
69
- return `T.${tv.type}`;
70
- }
71
- return null;
72
- case "unknown":
73
- return "T.unknown";
74
- case "never":
75
- return "T.never";
76
- case "literal": {
77
- const v = tv.value;
78
- if (typeof v === "number") {
79
- // NaN / Infinity / 1e21 等序列化后文法解析不回同值
80
- if (!Number.isFinite(v)) return null;
81
- const s = String(v);
82
- return NUMBER_LITERAL_REGEX.test(s) ? s : null;
83
- }
84
- if (typeof v === "boolean") return v ? "true" : "false";
85
- if (v === null) return "null";
86
- if (v === undefined) return "undefined";
87
- // bigint 字面量不可固化:文法无 bigint 字面量支持,序列化会退化成字符串
88
- if (typeof v === "bigint") return null;
89
- return serializeStringLiteral(v);
90
- }
91
- case "union": {
92
- const parts: string[] = [];
93
- for (const member of tv.members) {
94
- const s = serializeCaseArg(member);
95
- if (s === null) return null;
96
- parts.push(s);
97
- }
98
- return `T.union(${parts.join(", ")})`;
99
- }
100
- case "array": {
101
- const el = serializeCaseArg(tv.element);
102
- return el === null ? null : `T.array(${el})`;
103
- }
104
- case "tuple": {
105
- const parts: string[] = [];
106
- for (const el of tv.elements) {
107
- const s = serializeCaseArg(el);
108
- if (s === null) return null;
109
- parts.push(s);
110
- }
111
- return `[${parts.join(", ")}]`;
112
- }
113
- case "object": {
114
- const parts: string[] = [];
115
- for (const [key, value] of Object.entries(tv.properties)) {
116
- const vs = serializeCaseArg(value);
117
- if (vs === null) return null;
118
- const ks = serializeObjectKey(key);
119
- if (ks === null) return null;
120
- parts.push(`${ks}: ${vs}`);
121
- }
122
- return parts.length === 0 ? "{}" : `{ ${parts.join(", ")} }`;
123
- }
124
- default:
125
- // function / promise / instance / refined:指令文法不可表达
126
- return null;
127
- }
128
- }
129
-
130
- /**
131
- * 组装单行 ` * @nudo:case "name" (a, b)` 指令文本(无尾换行)。
132
- * 任一实参不可序列化、或名字含双引号/换行(名字正则 `"([^"]+)"` 承载不了)→ 整体 null。
133
- */
134
- export function buildCaseDirective(name: string, args: TypeValue[]): string | null {
135
- if (name.includes('"') || /[\r\n]/.test(name)) return null;
136
- const parts: string[] = [];
137
- for (const arg of args) {
138
- const s = serializeCaseArg(arg);
139
- if (s === null) return null;
140
- parts.push(s);
141
- }
142
- return ` * @nudo:case "${name}" (${parts.join(", ")})`;
143
- }
144
-
145
- /**
146
- * 从源码剥离所有本模块生成的 @nudo:case 指令(名字以 call@ 开头,整行删除)。
147
- * 若所属 JSDoc 块因此只剩空 ` *` 行(无其他 @nudo:* 指令、无文字内容),
148
- * 连块首 `/**` 行与块尾行整块删除。绝不碰非 case 指令与普通注释。
149
- * 注意:手写但以 call@ 命名的 case 同样会被删——call@ 前缀保留为生成物标记。
150
- * removed 返回被删指令名列表(按出现顺序)。
151
- */
152
- export function stripGeneratedCaseDirectives(source: string): { source: string; removed: string[] } {
153
- const lines = source.split("\n");
154
- const removed: string[] = [];
155
- const drop = new Array<boolean>(lines.length).fill(false);
156
-
157
- // 第一遍:标记 call@ 指令行
158
- for (let i = 0; i < lines.length; i++) {
159
- const m = lines[i].match(GENERATED_CASE_LINE_REGEX);
160
- if (m) {
161
- drop[i] = true;
162
- removed.push(m[1]);
163
- }
164
- }
165
-
166
- // 第二遍:删除后只剩空星号行的 JSDoc 块整体移除
167
- for (let i = 0; i < lines.length; i++) {
168
- // 开行必须是纯 `/**`(带文字的块不是本模块生成形状)
169
- if (!/^\s*\/\*\*\s*$/.test(lines[i])) continue;
170
- let j = i + 1;
171
- let clean = true;
172
- while (j < lines.length) {
173
- if (/\*\//.test(lines[j])) break;
174
- if (/\/\*/.test(lines[j])) {
175
- clean = false; // 块内异常嵌套,保守放弃
176
- break;
177
- }
178
- j++;
179
- }
180
- if (!clean || j >= lines.length) continue;
181
- // 结束行必须是纯 `*/`(如 `* text */` 的块结构不干净,不动)
182
- if (!/^\s*\*\/\s*$/.test(lines[j])) continue;
183
- let onlyFiller = true;
184
- for (let k = i + 1; k < j; k++) {
185
- if (drop[k]) continue;
186
- const t = lines[k].trim();
187
- if (t !== "" && !/^\*{1,2}$/.test(t)) {
188
- onlyFiller = false;
189
- break;
190
- }
191
- }
192
- if (onlyFiller) {
193
- for (let k = i; k <= j; k++) drop[k] = true;
194
- }
195
- }
196
-
197
- return {
198
- source: lines.filter((_, i) => !drop[i]).join("\n"),
199
- removed,
200
- };
201
- }
202
-
203
- export type EmitSkipReason =
204
- | "hand-written"
205
- | "already-generated"
206
- | "entry-only"
207
- | "no-serializable-cases"
208
- | "no-declaration"
209
- | "skipped";
210
-
211
- export type EmitResult = {
212
- source: string;
213
- changed: boolean;
214
- written: Array<{ fn: string; cases: string[] }>;
215
- skipped: Array<{ fn: string; reason: EmitSkipReason; detail?: string }>;
216
- };
217
-
218
- /** 源码里某函数名下已有的 case 指令(跨同名顶层语句合并收集) */
219
- function collectExistingCases(source: string): Map<string, CaseDirective[]> {
220
- const byName = new Map<string, CaseDirective[]>();
221
- let ast;
222
- try {
223
- ast = parse(source, { errorRecovery: true });
224
- } catch {
225
- return byName; // 源码解析失败时按"无已有指令"处理(analysis 本就来自同一份源码)
226
- }
227
- for (const { name, directives } of extractDirectives(ast)) {
228
- for (const d of directives) {
229
- if (d.kind !== "case") continue;
230
- const list = byName.get(name);
231
- if (list) list.push(d);
232
- else byName.set(name, [d]);
233
- }
234
- }
235
- return byName;
236
- }
237
-
238
- /**
239
- * 把 analysis 中的合成 case(source === "callsite")固化为源码指令:
240
- *
241
- * 1. 源码已有任何非 call@ 命名的 case → skip "hand-written";
242
- * 已有 call@ case → skip "already-generated"。
243
- * 2. fn.skipped / fn.noDeclaration / fn.entryOnly → 对应 skip。
244
- * 3. 逐 case 序列化;个别不可序列化的丢弃并在 skipped 记
245
- * no-serializable-cases(函数整体仍写可序列化子集,全不可序列化才整函数跳过)。
246
- * 4. 写入位置:函数声明行正上方——已有 JSDoc 块则插到 `/**` 行后,
247
- * 无块则新建三行块。缩进取声明行的 loc.start.column(babel 0 基列)。
248
- * 多函数编辑按行号从下往上应用,避免行号漂移。
249
- */
250
- export function insertGeneratedCaseDirectives(source: string, analysis: AnalysisResult): EmitResult {
251
- const written: EmitResult["written"] = [];
252
- const skipped: EmitResult["skipped"] = [];
253
- const existing = collectExistingCases(source);
254
-
255
- type Edit = { line: number; column: number; directives: string[]; fn: string };
256
- const edits: Edit[] = [];
257
-
258
- for (const fn of analysis.functions) {
259
- const existingCases = existing.get(fn.name) ?? [];
260
- if (existingCases.some((c) => !c.name.startsWith(GENERATED_PREFIX))) {
261
- skipped.push({ fn: fn.name, reason: "hand-written" });
262
- continue;
263
- }
264
- if (existingCases.length > 0) {
265
- skipped.push({ fn: fn.name, reason: "already-generated" });
266
- continue;
267
- }
268
- if (fn.skipped) {
269
- skipped.push({ fn: fn.name, reason: "skipped" });
270
- continue;
271
- }
272
- if (fn.noDeclaration) {
273
- skipped.push({ fn: fn.name, reason: "no-declaration" });
274
- continue;
275
- }
276
- if (fn.entryOnly) {
277
- skipped.push({ fn: fn.name, reason: "entry-only" });
278
- continue;
279
- }
280
- const callsiteCases = fn.cases.filter((c) => c.source === "callsite");
281
- if (callsiteCases.length === 0) {
282
- skipped.push({ fn: fn.name, reason: "no-serializable-cases", detail: "no callsite cases" });
283
- continue;
284
- }
285
- const built: string[] = [];
286
- const names: string[] = [];
287
- for (const c of callsiteCases) {
288
- const directive = buildCaseDirective(c.name, c.args);
289
- if (directive === null) {
290
- skipped.push({ fn: fn.name, reason: "no-serializable-cases", detail: `case ${c.name} not serializable` });
291
- } else {
292
- built.push(directive);
293
- names.push(c.name);
294
- }
295
- }
296
- if (built.length === 0) continue; // 全部不可序列化:上方已逐 case 记录 skip
297
- edits.push({ line: fn.loc.start.line, column: fn.loc.start.column, directives: built, fn: fn.name });
298
- written.push({ fn: fn.name, cases: names });
299
- }
300
-
301
- // 从下往上应用编辑
302
- edits.sort((a, b) => b.line - a.line);
303
- const lines = source.split("\n");
304
- for (const edit of edits) {
305
- const declIdx = edit.line - 1;
306
- const indent = " ".repeat(edit.column);
307
-
308
- // 声明行正上方是否挂着可注入的 JSDoc 块(结束行必须是纯 `*/`)
309
- let insertIdx = -1;
310
- let blockIndent = "";
311
- const aboveIdx = declIdx - 1;
312
- if (aboveIdx >= 0 && /^\s*\*\/\s*$/.test(lines[aboveIdx])) {
313
- let s = aboveIdx - 1;
314
- while (s >= 0) {
315
- const t = lines[s];
316
- if (/^\s*\/\*\*/.test(t)) {
317
- insertIdx = s + 1; // 指令行插到 `/**` 行后第一行
318
- blockIndent = t.match(/^\s*/)?.[0] ?? "";
319
- break;
320
- }
321
- // 块内部行:星号续行 / 空行 / 块尾;撞到其他内容说明上面不是干净的注释块
322
- if (/^\s*\*/.test(t) || t.trim() === "") {
323
- s--;
324
- continue;
325
- }
326
- break;
327
- }
328
- }
329
-
330
- if (insertIdx >= 0) {
331
- lines.splice(insertIdx, 0, ...edit.directives.map((d) => blockIndent + d));
332
- } else {
333
- lines.splice(declIdx, 0, `${indent}/**`, ...edit.directives.map((d) => indent + d), `${indent} */`);
334
- }
335
- }
336
-
337
- return {
338
- source: lines.join("\n"),
339
- changed: written.length > 0,
340
- written,
341
- skipped,
342
- };
343
- }
344
-
345
- // ---------------------------------------------------------------------------
346
- // unified diff(行级 LCS,无第三方依赖)
347
- // ---------------------------------------------------------------------------
348
-
349
- type DiffOp = { type: "equal" | "delete" | "insert"; text: string };
350
-
351
- /** 行级 LCS:先裁公共前后缀把 DP 表压到真实差异窗口,再回溯产出操作序列 */
352
- function diffOps(a: string[], b: string[]): DiffOp[] {
353
- let start = 0;
354
- while (start < a.length && start < b.length && a[start] === b[start]) start++;
355
- let endA = a.length;
356
- let endB = b.length;
357
- while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
358
- endA--;
359
- endB--;
360
- }
361
- const midA = a.slice(start, endA);
362
- const midB = b.slice(start, endB);
363
- const n = midA.length;
364
- const m = midB.length;
365
-
366
- // dp[i*(m+1)+j] = midA[i:] 与 midB[j:] 的 LCS 长度
367
- const dp = new Int32Array((n + 1) * (m + 1));
368
- for (let i = n - 1; i >= 0; i--) {
369
- for (let j = m - 1; j >= 0; j--) {
370
- dp[i * (m + 1) + j] =
371
- midA[i] === midB[j]
372
- ? dp[(i + 1) * (m + 1) + (j + 1)] + 1
373
- : Math.max(dp[(i + 1) * (m + 1) + j], dp[i * (m + 1) + (j + 1)]);
374
- }
375
- }
376
-
377
- const ops: DiffOp[] = [];
378
- for (let k = 0; k < start; k++) ops.push({ type: "equal", text: a[k] });
379
- let i = 0;
380
- let j = 0;
381
- while (i < n && j < m) {
382
- if (midA[i] === midB[j]) {
383
- ops.push({ type: "equal", text: midA[i] });
384
- i++;
385
- j++;
386
- } else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + (j + 1)]) {
387
- ops.push({ type: "delete", text: midA[i] });
388
- i++;
389
- } else {
390
- ops.push({ type: "insert", text: midB[j] });
391
- j++;
392
- }
393
- }
394
- while (i < n) ops.push({ type: "delete", text: midA[i++] });
395
- while (j < m) ops.push({ type: "insert", text: midB[j++] });
396
- for (let k = endA; k < a.length; k++) ops.push({ type: "equal", text: a[k] });
397
- return ops;
398
- }
399
-
400
- const DIFF_CONTEXT = 3;
401
-
402
- /** `@@ -1,3 +1,4 @@` 风格的范围描述;count 为 1 时省略 `,c`,为 0 时行号回退一行 */
403
- function formatRange(start: number, count: number): string {
404
- const s = count === 0 ? start - 1 : start;
405
- return count === 1 ? `${s}` : `${s},${count}`;
406
- }
407
-
408
- /** 行级 unified diff:`--- a/path` 头 + `@@` hunk + 上下文 3 行;相同返回 "" */
409
- export function unifiedDiff(a: string, b: string, path: string): string {
410
- if (a === b) return "";
411
- const ops = diffOps(a.split("\n"), b.split("\n"));
412
-
413
- // 切 hunk:变更之间相等行 ≤ 2*context 时合并为一个 hunk
414
- const hunks: Array<{ start: number; ops: DiffOp[] }> = [];
415
- let idx = 0;
416
- while (idx < ops.length) {
417
- if (ops[idx].type === "equal") {
418
- idx++;
419
- continue;
420
- }
421
- let start = idx;
422
- let ctx = 0;
423
- while (start > 0 && ops[start - 1].type === "equal" && ctx < DIFF_CONTEXT) {
424
- start--;
425
- ctx++;
426
- }
427
- let j = idx;
428
- let equalRun = 0;
429
- let lastChange = idx;
430
- while (j < ops.length) {
431
- if (ops[j].type === "equal") {
432
- equalRun++;
433
- if (equalRun > DIFF_CONTEXT * 2) break;
434
- } else {
435
- equalRun = 0;
436
- lastChange = j;
437
- }
438
- j++;
439
- }
440
- let end = lastChange + 1;
441
- ctx = 0;
442
- while (end < ops.length && ops[end].type === "equal" && ctx < DIFF_CONTEXT) {
443
- end++;
444
- ctx++;
445
- }
446
- hunks.push({ start, ops: ops.slice(start, end) });
447
- idx = end;
448
- }
449
-
450
- // hunk 头行号:hunk 前累计的 delete+equal 决定旧侧、insert+equal 决定新侧
451
- const out: string[] = [`--- a/${path}`, `+++ b/${path}`];
452
- for (let h = 0; h < hunks.length; h++) {
453
- const { start, ops: hunkOps } = hunks[h];
454
- let oldCount = 0;
455
- let newCount = 0;
456
- for (let k = 0; k < start; k++) {
457
- if (ops[k].type !== "insert") oldCount++;
458
- if (ops[k].type !== "delete") newCount++;
459
- }
460
- let dels = 0;
461
- let ins = 0;
462
- for (const op of hunkOps) {
463
- if (op.type === "delete") dels++;
464
- else if (op.type === "insert") ins++;
465
- else {
466
- dels++;
467
- ins++;
468
- }
469
- }
470
- out.push(`@@ -${formatRange(oldCount + 1, dels)} +${formatRange(newCount + 1, ins)} @@`);
471
- for (const op of hunkOps) {
472
- const prefix = op.type === "equal" ? " " : op.type === "delete" ? "-" : "+";
473
- out.push(prefix + op.text);
474
- }
475
- }
476
- return out.join("\n") + "\n";
477
- }