@spaceflow/review 0.77.0 → 0.79.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/CHANGELOG.md +67 -0
- package/dist/index.js +1095 -500
- package/package.json +1 -1
- package/src/deletion-impact.service.ts +13 -128
- package/src/issue-verify.service.ts +18 -82
- package/src/mcp/index.ts +4 -1
- package/src/prompt/code-review.ts +95 -0
- package/src/prompt/deletion-impact.ts +105 -0
- package/src/prompt/index.ts +37 -0
- package/src/prompt/issue-verify.ts +86 -0
- package/src/prompt/pr-description.ts +149 -0
- package/src/prompt/schemas.ts +106 -0
- package/src/prompt/types.ts +53 -0
- package/src/review-context.ts +53 -15
- package/src/review-includes-filter.spec.ts +36 -0
- package/src/review-includes-filter.ts +59 -7
- package/src/review-issue-filter.ts +1 -1
- package/src/review-llm.ts +116 -207
- package/src/review-result-model.ts +28 -6
- package/src/review-spec/review-spec.service.ts +26 -5
- package/src/review.config.ts +31 -5
- package/src/review.service.spec.ts +75 -3
- package/src/review.service.ts +120 -13
- package/src/system-rules/index.ts +48 -0
- package/src/system-rules/max-lines-per-file.ts +57 -0
- package/src/types/review-llm.ts +2 -0
- package/src/utils/review-llm.spec.ts +277 -0
- package/src/utils/review-llm.ts +152 -7
- package/src/utils/review-pr-comment.spec.ts +340 -0
- package/src/{review-pr-comment-utils.ts → utils/review-pr-comment.ts} +2 -2
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildLinesWithNumbers, buildCommitsSection, extractCodeBlocks } from "./review-llm";
|
|
3
|
+
|
|
4
|
+
describe("utils/review-llm", () => {
|
|
5
|
+
describe("buildLinesWithNumbers", () => {
|
|
6
|
+
const lines: [string, string][] = [
|
|
7
|
+
["abc1234", "line one"],
|
|
8
|
+
["-------", "line two"],
|
|
9
|
+
["abc1234", "line three"],
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
it("无 visibleRanges 时输出全部行,带行号和 hash", () => {
|
|
13
|
+
expect(buildLinesWithNumbers(lines)).toBe(
|
|
14
|
+
"abc1234 1| line one\n------- 2| line two\nabc1234 3| line three",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("行号宽度按总行数自动 pad(10 行时个位行号前有空格)", () => {
|
|
19
|
+
const tenLines: [string, string][] = Array.from({ length: 10 }, (_, i) => [
|
|
20
|
+
"abc1234",
|
|
21
|
+
`line ${i + 1}`,
|
|
22
|
+
]);
|
|
23
|
+
const result = buildLinesWithNumbers(tenLines);
|
|
24
|
+
expect(result.split("\n")[0]).toContain(" 1|");
|
|
25
|
+
expect(result.split("\n")[9]).toContain("10|");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("visibleRanges 为空数组时等价于无 visibleRanges,输出全部行", () => {
|
|
29
|
+
expect(buildLinesWithNumbers(lines, [])).toBe(buildLinesWithNumbers(lines));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("只指定首行时,末尾被忽略区间产生 ignore 占位", () => {
|
|
33
|
+
const result = buildLinesWithNumbers(lines, [[1, 1]]);
|
|
34
|
+
expect(result).toBe("abc1234 1| line one\n....... ignore 2-3 line .......");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("只指定末行时,开头被忽略区间产生 ignore 占位", () => {
|
|
38
|
+
const result = buildLinesWithNumbers(lines, [[3, 3]]);
|
|
39
|
+
expect(result).toBe("....... ignore 1-2 line .......\nabc1234 3| line three");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("只指定中间行时,前后均产生 ignore 占位", () => {
|
|
43
|
+
const result = buildLinesWithNumbers(lines, [[2, 2]]);
|
|
44
|
+
expect(result).toBe(
|
|
45
|
+
"....... ignore 1-1 line .......\n------- 2| line two\n....... ignore 3-3 line .......",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("多个不相邻范围各自产生 ignore 占位", () => {
|
|
50
|
+
const fiveLines: [string, string][] = [
|
|
51
|
+
["abc1234", "a"],
|
|
52
|
+
["-------", "b"],
|
|
53
|
+
["abc1234", "c"],
|
|
54
|
+
["-------", "d"],
|
|
55
|
+
["abc1234", "e"],
|
|
56
|
+
];
|
|
57
|
+
const result = buildLinesWithNumbers(fiveLines, [
|
|
58
|
+
[1, 1],
|
|
59
|
+
[5, 5],
|
|
60
|
+
]);
|
|
61
|
+
expect(result).toBe("abc1234 1| a\n....... ignore 2-4 line .......\nabc1234 5| e");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("重叠的 visibleRanges 合并后不产生 ignore 占位", () => {
|
|
65
|
+
const result = buildLinesWithNumbers(lines, [
|
|
66
|
+
[1, 2],
|
|
67
|
+
[2, 3],
|
|
68
|
+
]);
|
|
69
|
+
expect(result).not.toContain("ignore");
|
|
70
|
+
expect(result.split("\n")).toHaveLength(3);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("visibleRanges 超出文件范围时自动裁剪,不产生 ignore 占位", () => {
|
|
74
|
+
const result = buildLinesWithNumbers(lines, [[0, 100]]);
|
|
75
|
+
expect(result).not.toContain("ignore");
|
|
76
|
+
expect(result.split("\n")).toHaveLength(3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("visibleRanges 无序时按起始行号排序后正常输出", () => {
|
|
80
|
+
const result = buildLinesWithNumbers(lines, [
|
|
81
|
+
[3, 3],
|
|
82
|
+
[1, 1],
|
|
83
|
+
]);
|
|
84
|
+
expect(result).toBe(
|
|
85
|
+
"abc1234 1| line one\n....... ignore 2-2 line .......\nabc1234 3| line three",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("extractCodeBlocks", () => {
|
|
91
|
+
it("types 为空时返回空数组", () => {
|
|
92
|
+
expect(extractCodeBlocks([["abc1234", "function foo() {}"]], [])).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("空 contentLines 返回空数组", () => {
|
|
96
|
+
expect(extractCodeBlocks([], ["function"])).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("全为上下文行时返回空数组", () => {
|
|
100
|
+
const lines: [string, string][] = [
|
|
101
|
+
["-------", "function foo() { }"],
|
|
102
|
+
["-------", "class Bar { }"],
|
|
103
|
+
];
|
|
104
|
+
expect(extractCodeBlocks(lines, ["function", "class"])).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("提取新增的单行 function", () => {
|
|
108
|
+
expect(
|
|
109
|
+
extractCodeBlocks([["abc1234", "function foo() { return 1; }"]], ["function"]),
|
|
110
|
+
).toEqual([[1, 1]]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("提取新增的多行 function", () => {
|
|
114
|
+
const lines: [string, string][] = [
|
|
115
|
+
["abc1234", "function foo() {"],
|
|
116
|
+
["abc1234", " return 1;"],
|
|
117
|
+
["abc1234", "}"],
|
|
118
|
+
];
|
|
119
|
+
expect(extractCodeBlocks(lines, ["function"])).toEqual([[1, 3]]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("提取 export function", () => {
|
|
123
|
+
expect(extractCodeBlocks([["abc1234", "export function hello() { }"]], ["function"])).toEqual(
|
|
124
|
+
[[1, 1]],
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("提取 async function", () => {
|
|
129
|
+
expect(extractCodeBlocks([["abc1234", "async function load() { }"]], ["function"])).toEqual([
|
|
130
|
+
[1, 1],
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("上下文行中的 function 不被提取,只提取新增行中的", () => {
|
|
135
|
+
const lines: [string, string][] = [
|
|
136
|
+
["-------", "function foo() {"],
|
|
137
|
+
["-------", " return 1;"],
|
|
138
|
+
["-------", "}"],
|
|
139
|
+
["abc1234", "function bar() { return 2; }"],
|
|
140
|
+
];
|
|
141
|
+
expect(extractCodeBlocks(lines, ["function"])).toEqual([[4, 4]]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("提取多行 class 代码块", () => {
|
|
145
|
+
const lines: [string, string][] = [
|
|
146
|
+
["abc1234", "class Foo {"],
|
|
147
|
+
["abc1234", " bar() {}"],
|
|
148
|
+
["abc1234", "}"],
|
|
149
|
+
];
|
|
150
|
+
expect(extractCodeBlocks(lines, ["class"])).toEqual([[1, 3]]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("提取 export class", () => {
|
|
154
|
+
expect(extractCodeBlocks([["abc1234", "export class Bar { }"]], ["class"])).toEqual([[1, 1]]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("提取多行 interface", () => {
|
|
158
|
+
const lines: [string, string][] = [
|
|
159
|
+
["abc1234", "export interface IFoo {"],
|
|
160
|
+
["abc1234", " name: string;"],
|
|
161
|
+
["abc1234", "}"],
|
|
162
|
+
];
|
|
163
|
+
expect(extractCodeBlocks(lines, ["interface"])).toEqual([[1, 3]]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("提取 type 别名(含 =)", () => {
|
|
167
|
+
expect(
|
|
168
|
+
extractCodeBlocks([["abc1234", "export type MyType = string | number;"]], ["type"]),
|
|
169
|
+
).toEqual([[1, 1]]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("提取泛型 type(含 <)", () => {
|
|
173
|
+
expect(extractCodeBlocks([["abc1234", "type Result<T> = { data: T };"]], ["type"])).toEqual([
|
|
174
|
+
[1, 1],
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("同时提取 function 和 class,中间上下文行不影响结果", () => {
|
|
179
|
+
const lines: [string, string][] = [
|
|
180
|
+
["abc1234", "function foo() { }"],
|
|
181
|
+
["-------", "const x = 1;"],
|
|
182
|
+
["abc1234", "class Bar { }"],
|
|
183
|
+
];
|
|
184
|
+
expect(extractCodeBlocks(lines, ["function", "class"])).toEqual([
|
|
185
|
+
[1, 1],
|
|
186
|
+
[3, 3],
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("相邻代码块合并为一个范围", () => {
|
|
191
|
+
const lines: [string, string][] = [
|
|
192
|
+
["abc1234", "function a() {"],
|
|
193
|
+
["abc1234", "}"],
|
|
194
|
+
["abc1234", "function b() {"],
|
|
195
|
+
["abc1234", "}"],
|
|
196
|
+
];
|
|
197
|
+
expect(extractCodeBlocks(lines, ["function"])).toEqual([[1, 4]]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("嵌套括号时找到最外层封闭括号作为结尾", () => {
|
|
201
|
+
const lines: [string, string][] = [
|
|
202
|
+
["abc1234", "function outer() {"],
|
|
203
|
+
["abc1234", " if (true) {"],
|
|
204
|
+
["abc1234", " inner();"],
|
|
205
|
+
["abc1234", " }"],
|
|
206
|
+
["abc1234", "}"],
|
|
207
|
+
];
|
|
208
|
+
expect(extractCodeBlocks(lines, ["function"])).toEqual([[1, 5]]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("method:public 修饰的方法识别为完整代码块", () => {
|
|
212
|
+
const lines: [string, string][] = [
|
|
213
|
+
["abc1234", " public getName() {"],
|
|
214
|
+
["abc1234", " return this.name;"],
|
|
215
|
+
["abc1234", " }"],
|
|
216
|
+
];
|
|
217
|
+
expect(extractCodeBlocks(lines, ["method"])).toEqual([[1, 3]]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("method:async 方法识别为完整代码块", () => {
|
|
221
|
+
const lines: [string, string][] = [
|
|
222
|
+
["abc1234", " async fetchData() {"],
|
|
223
|
+
["abc1234", " return await api();"],
|
|
224
|
+
["abc1234", " }"],
|
|
225
|
+
];
|
|
226
|
+
expect(extractCodeBlocks(lines, ["method"])).toHaveLength(1);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("buildCommitsSection", () => {
|
|
231
|
+
const lines: [string, string][] = [
|
|
232
|
+
["abc1234", "const x = 1;"],
|
|
233
|
+
["-------", "const y = 2;"],
|
|
234
|
+
["def5678", "const z = 3;"],
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
it("有匹配 commit 时返回 markdown 列表(- `hash` 首行消息)", () => {
|
|
238
|
+
const commits = [
|
|
239
|
+
{ sha: "abc1234abcdef", commit: { message: "feat: add x\n详细说明" } },
|
|
240
|
+
{ sha: "def5678abcdef", commit: { message: "fix: fix z" } },
|
|
241
|
+
] as any;
|
|
242
|
+
const result = buildCommitsSection(lines, commits);
|
|
243
|
+
expect(result).toContain("- `abc1234` feat: add x");
|
|
244
|
+
expect(result).toContain("- `def5678` fix: fix z");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("commit 消息只取第一行,忽略后续内容", () => {
|
|
248
|
+
const commits = [
|
|
249
|
+
{ sha: "abc1234abc", commit: { message: "first line\nsecond line\nthird line" } },
|
|
250
|
+
] as any;
|
|
251
|
+
expect(buildCommitsSection(lines, commits)).toBe("- `abc1234` first line");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("没有匹配 commit 时返回默认文案", () => {
|
|
255
|
+
const commits = [{ sha: "xxxxxxxxxxxxxxx", commit: { message: "unrelated" } }] as any;
|
|
256
|
+
expect(buildCommitsSection(lines, commits)).toBe("- 无相关 commits");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("commits 为空数组时返回默认文案", () => {
|
|
260
|
+
expect(buildCommitsSection(lines, [])).toBe("- 无相关 commits");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("contentLines 全为上下文行时返回默认文案", () => {
|
|
264
|
+
const ctxLines: [string, string][] = [
|
|
265
|
+
["-------", "line 1"],
|
|
266
|
+
["-------", "line 2"],
|
|
267
|
+
];
|
|
268
|
+
const commits = [{ sha: "abc1234abc", commit: { message: "msg" } }] as any;
|
|
269
|
+
expect(buildCommitsSection(ctxLines, commits)).toBe("- 无相关 commits");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("commit sha 为 undefined 时不崩溃,返回默认文案", () => {
|
|
273
|
+
const commits = [{ sha: undefined, commit: { message: "msg" } }] as any;
|
|
274
|
+
expect(buildCommitsSection(lines, commits)).toBe("- 无相关 commits");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
package/src/utils/review-llm.ts
CHANGED
|
@@ -1,13 +1,158 @@
|
|
|
1
1
|
import type { PullRequestCommit } from "@spaceflow/core";
|
|
2
|
+
import type { CodeBlockType } from "../review-includes-filter";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* 构建带行号的文件内容字符串。
|
|
6
|
+
*
|
|
7
|
+
* @param contentLines [hash, code] 行数组
|
|
8
|
+
* @param visibleRanges 可选,指定需要输出的行号区间 [startLine, endLine](不含则输出全文)
|
|
9
|
+
* 被跳过的连续行用 `...... ..| ignore {start}-{end} code` 占位
|
|
10
|
+
*/
|
|
11
|
+
export function buildLinesWithNumbers(
|
|
12
|
+
contentLines: [string, string][],
|
|
13
|
+
visibleRanges?: [number, number][],
|
|
14
|
+
): string {
|
|
4
15
|
const padWidth = String(contentLines.length).length;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
|
|
17
|
+
if (!visibleRanges || visibleRanges.length === 0) {
|
|
18
|
+
return contentLines
|
|
19
|
+
.map(([hash, line], index) => {
|
|
20
|
+
const lineNum = index + 1;
|
|
21
|
+
return `${hash} ${String(lineNum).padStart(padWidth)}| ${line}`;
|
|
22
|
+
})
|
|
23
|
+
.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 将 ranges 按起始行号排序并合并重叠区间
|
|
27
|
+
const sorted = [...visibleRanges].sort((a, b) => a[0] - b[0]);
|
|
28
|
+
const merged: [number, number][] = [];
|
|
29
|
+
for (const range of sorted) {
|
|
30
|
+
if (merged.length > 0 && range[0] <= merged[merged.length - 1][1] + 1) {
|
|
31
|
+
merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], range[1]);
|
|
32
|
+
} else {
|
|
33
|
+
merged.push([...range]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const output: string[] = [];
|
|
38
|
+
let prevEnd = 0;
|
|
39
|
+
|
|
40
|
+
for (const [start, end] of merged) {
|
|
41
|
+
const clampedStart = Math.max(1, start);
|
|
42
|
+
const clampedEnd = Math.min(contentLines.length, end);
|
|
43
|
+
|
|
44
|
+
// 被忽略的前缀区间
|
|
45
|
+
if (clampedStart > prevEnd + 1) {
|
|
46
|
+
output.push(`....... ignore ${prevEnd + 1}-${clampedStart - 1} line .......`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 输出可见行
|
|
50
|
+
for (let i = clampedStart - 1; i < clampedEnd; i++) {
|
|
51
|
+
const [hash, line] = contentLines[i];
|
|
52
|
+
const lineNum = i + 1;
|
|
53
|
+
output.push(`${hash} ${String(lineNum).padStart(padWidth)}| ${line}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
prevEnd = clampedEnd;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 被忽略的末尾区间
|
|
60
|
+
if (prevEnd < contentLines.length) {
|
|
61
|
+
output.push(`....... ignore ${prevEnd + 1}-${contentLines.length} line .......`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return output.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 从 contentLines 中提取新增代码里的指定结构类型的行号范围。
|
|
69
|
+
*
|
|
70
|
+
* 逻辑:
|
|
71
|
+
* 1. 只考虑 hash !== "-------" 的新增行
|
|
72
|
+
* 2. 用各类型的正则匹配结构开头行,再用层级计数找到结尾行
|
|
73
|
+
* 3. 返回行号范围列表 [startLine, endLine](从 1 计)
|
|
74
|
+
*
|
|
75
|
+
* @param contentLines 文件的 [hash, code] 行列表
|
|
76
|
+
* @param types 要提取的结构类型
|
|
77
|
+
*/
|
|
78
|
+
export function extractCodeBlocks(
|
|
79
|
+
contentLines: [string, string][],
|
|
80
|
+
types: CodeBlockType[],
|
|
81
|
+
): [number, number][] {
|
|
82
|
+
if (types.length === 0) return [];
|
|
83
|
+
|
|
84
|
+
const ranges: [number, number][] = [];
|
|
85
|
+
|
|
86
|
+
// 将所有行的实际代码组成文本(用于层级计数)
|
|
87
|
+
const fullLines = contentLines.map(([, code]) => code);
|
|
88
|
+
|
|
89
|
+
// 各类型的开头識别正则(匹配行首)
|
|
90
|
+
const PATTERNS: Record<CodeBlockType, RegExp> = {
|
|
91
|
+
function: /^\s*(?:export\s+)?(?:async\s+)?function\s+\w+/,
|
|
92
|
+
class: /^\s*(?:export\s+)?(?:abstract\s+)?class\s+\w+/,
|
|
93
|
+
interface: /^\s*(?:export\s+)?interface\s+\w+/,
|
|
94
|
+
type: /^\s*(?:export\s+)?type\s+\w+\s*[=<]/,
|
|
95
|
+
method:
|
|
96
|
+
/^\s*(?:(?:public|protected|private|static|async|override|readonly|abstract)\s+)*(?!(?:if|for|while|switch|return|const|let|var|throw|new)\b)(\w+)\s*[(<]/,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < fullLines.length; i++) {
|
|
100
|
+
const lineNum = i + 1;
|
|
101
|
+
const isAdded = contentLines[i][0] !== "-------";
|
|
102
|
+
if (!isAdded) continue;
|
|
103
|
+
|
|
104
|
+
for (const type of types) {
|
|
105
|
+
const pattern = PATTERNS[type];
|
|
106
|
+
if (!pattern.test(fullLines[i])) continue;
|
|
107
|
+
|
|
108
|
+
// 找到结构开头,用层级计数找封闭括号结尾
|
|
109
|
+
const endLine = findBlockEnd(fullLines, i);
|
|
110
|
+
ranges.push([lineNum, endLine]);
|
|
111
|
+
break; // 同一行只匹配一种类型
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return mergeRanges(ranges);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 从开始行向下层级计数,找到匹配的封闭括号位置(行号从 1 计)。
|
|
120
|
+
* 如果没有找到匹配括号,返回开始行到文件末尾。
|
|
121
|
+
*/
|
|
122
|
+
function findBlockEnd(lines: string[], startIndex: number): number {
|
|
123
|
+
let depth = 0;
|
|
124
|
+
let foundOpen = false;
|
|
125
|
+
|
|
126
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
127
|
+
for (const ch of lines[i]) {
|
|
128
|
+
if (ch === "{") {
|
|
129
|
+
depth++;
|
|
130
|
+
foundOpen = true;
|
|
131
|
+
} else if (ch === "}") {
|
|
132
|
+
depth--;
|
|
133
|
+
if (foundOpen && depth === 0) {
|
|
134
|
+
return i + 1; // 行号从 1 计
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return lines.length; // 没有找到匹配括号,返回文件末尾
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function mergeRanges(ranges: [number, number][]): [number, number][] {
|
|
144
|
+
if (ranges.length === 0) return [];
|
|
145
|
+
const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
146
|
+
const merged: [number, number][] = [sorted[0]];
|
|
147
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
148
|
+
const last = merged[merged.length - 1];
|
|
149
|
+
if (sorted[i][0] <= last[1] + 1) {
|
|
150
|
+
last[1] = Math.max(last[1], sorted[i][1]);
|
|
151
|
+
} else {
|
|
152
|
+
merged.push([...sorted[i]]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return merged;
|
|
11
156
|
}
|
|
12
157
|
|
|
13
158
|
export function buildCommitsSection(
|