semantic-code-review 0.24.2__py3-none-any.whl
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.
- semantic_code_review/__init__.py +5 -0
- semantic_code_review/augment/__init__.py +3 -0
- semantic_code_review/augment/agents.py +95 -0
- semantic_code_review/augment/console.py +440 -0
- semantic_code_review/augment/extra_review.py +237 -0
- semantic_code_review/augment/fold_summary.py +447 -0
- semantic_code_review/augment/hunks.py +264 -0
- semantic_code_review/augment/mcp_server.py +156 -0
- semantic_code_review/augment/overview.py +233 -0
- semantic_code_review/augment/pass_.py +171 -0
- semantic_code_review/augment/pipeline.py +571 -0
- semantic_code_review/augment/progress.py +223 -0
- semantic_code_review/augment/prompts.py +93 -0
- semantic_code_review/augment/schemas.py +408 -0
- semantic_code_review/augment/tools.py +475 -0
- semantic_code_review/augment/trace_adapter.py +369 -0
- semantic_code_review/backends/__init__.py +95 -0
- semantic_code_review/backends/_cli_driver.py +571 -0
- semantic_code_review/backends/anthropic_sdk.py +56 -0
- semantic_code_review/backends/base.py +104 -0
- semantic_code_review/backends/claude_cli.py +421 -0
- semantic_code_review/backends/google_sdk.py +73 -0
- semantic_code_review/backends/openai_compat.py +42 -0
- semantic_code_review/cache/__init__.py +3 -0
- semantic_code_review/cache/store.py +87 -0
- semantic_code_review/cli/__init__.py +70 -0
- semantic_code_review/cli/_shared.py +157 -0
- semantic_code_review/cli/augment.py +74 -0
- semantic_code_review/cli/config_cmd.py +266 -0
- semantic_code_review/cli/fetch.py +33 -0
- semantic_code_review/cli/lint.py +27 -0
- semantic_code_review/cli/pr.py +98 -0
- semantic_code_review/cli/review.py +104 -0
- semantic_code_review/cli/runs_cmd.py +17 -0
- semantic_code_review/cli/show.py +19 -0
- semantic_code_review/cli/strip.py +20 -0
- semantic_code_review/config.py +563 -0
- semantic_code_review/config_template.py +154 -0
- semantic_code_review/fetch/__init__.py +61 -0
- semantic_code_review/fetch/anchor.py +244 -0
- semantic_code_review/fetch/github.py +229 -0
- semantic_code_review/fetch/github_comments.py +404 -0
- semantic_code_review/fetch/local.py +443 -0
- semantic_code_review/fetch/run_source.py +70 -0
- semantic_code_review/format/__init__.py +3 -0
- semantic_code_review/format/emit.py +163 -0
- semantic_code_review/format/lint.py +74 -0
- semantic_code_review/format/parse.py +574 -0
- semantic_code_review/format/sidecar.py +19 -0
- semantic_code_review/format/strip.py +20 -0
- semantic_code_review/git_ops.py +252 -0
- semantic_code_review/paths.py +61 -0
- semantic_code_review/review/__init__.py +3 -0
- semantic_code_review/review/comments.py +176 -0
- semantic_code_review/review/github.py +284 -0
- semantic_code_review/review/github_graphql.py +433 -0
- semantic_code_review/review/pr_flow.py +299 -0
- semantic_code_review/review/runner.py +407 -0
- semantic_code_review/review/server.py +1043 -0
- semantic_code_review/structural/__init__.py +41 -0
- semantic_code_review/structural/diff.py +102 -0
- semantic_code_review/structural/parse.py +330 -0
- semantic_code_review/structural/symbols.py +43 -0
- semantic_code_review/viewer/__init__.py +3 -0
- semantic_code_review/viewer/assets/annotations.ts +547 -0
- semantic_code_review/viewer/assets/boot.ts +273 -0
- semantic_code_review/viewer/assets/comment_store.ts +121 -0
- semantic_code_review/viewer/assets/comments.ts +624 -0
- semantic_code_review/viewer/assets/console.ts +426 -0
- semantic_code_review/viewer/assets/console_render.ts +213 -0
- semantic_code_review/viewer/assets/console_selection.ts +102 -0
- semantic_code_review/viewer/assets/data_store.ts +156 -0
- semantic_code_review/viewer/assets/file_rows.ts +49 -0
- semantic_code_review/viewer/assets/folds.ts +560 -0
- semantic_code_review/viewer/assets/index.html +62 -0
- semantic_code_review/viewer/assets/post_modal.ts +383 -0
- semantic_code_review/viewer/assets/progress.ts +138 -0
- semantic_code_review/viewer/assets/render.ts +937 -0
- semantic_code_review/viewer/assets/sidebar.ts +429 -0
- semantic_code_review/viewer/assets/sse.ts +78 -0
- semantic_code_review/viewer/assets/text_highlight.ts +215 -0
- semantic_code_review/viewer/assets/types.d.ts +388 -0
- semantic_code_review/viewer/assets/vendor/LICENSE +29 -0
- semantic_code_review/viewer/assets/vendor/VENDOR.md +55 -0
- semantic_code_review/viewer/assets/vendor/github-dark.min.css +10 -0
- semantic_code_review/viewer/assets/vendor/github.min.css +10 -0
- semantic_code_review/viewer/assets/vendor/highlight.min.js +1244 -0
- semantic_code_review/viewer/assets/vendor/mermaid.LICENSE +21 -0
- semantic_code_review/viewer/assets/vendor/mermaid.min.js +3587 -0
- semantic_code_review/viewer/assets/vendor/refresh.sh +65 -0
- semantic_code_review/viewer/assets/viewer.css +1202 -0
- semantic_code_review/viewer/assets/viewer.js +10588 -0
- semantic_code_review/viewer/build_json.py +546 -0
- semantic_code_review/viewer/hunk_layout.py +471 -0
- semantic_code_review-0.24.2.dist-info/METADATA +348 -0
- semantic_code_review-0.24.2.dist-info/RECORD +100 -0
- semantic_code_review-0.24.2.dist-info/WHEEL +5 -0
- semantic_code_review-0.24.2.dist-info/entry_points.txt +2 -0
- semantic_code_review-0.24.2.dist-info/licenses/LICENSE +21 -0
- semantic_code_review-0.24.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// Indent-based fold detection + on-demand fold-summary requests.
|
|
2
|
+
//
|
|
3
|
+
// The viewer's fold story is unified per-file (not per-stretch):
|
|
4
|
+
// `attachFileFolds(fileEl, file)` walks every visible row in the
|
|
5
|
+
// file body in DOM order — across hunks and adjacent expanded
|
|
6
|
+
// context blocks — runs an indent-based fold detector over the
|
|
7
|
+
// unified sequence, and attaches one chevron per region. Folds
|
|
8
|
+
// whose body spans a hunk boundary collapse the right rows in
|
|
9
|
+
// every container because each row carries its own DOM refs.
|
|
10
|
+
//
|
|
11
|
+
// First time the reviewer collapses a region whose summary is
|
|
12
|
+
// empty, this module fires `POST /fold-summary` against the live
|
|
13
|
+
// review server. The response writes back into the region object
|
|
14
|
+
// (mutating DATA in place); the server's `fold-summary` SSE event
|
|
15
|
+
// is handled by `applyFoldSummary` in boot.ts.
|
|
16
|
+
//
|
|
17
|
+
import { Annotations, type AnnotationHandle } from "./annotations";
|
|
18
|
+
import { FileRows, type RowWithEls } from "./file_rows";
|
|
19
|
+
|
|
20
|
+
interface DetectedRegion {
|
|
21
|
+
header_idx: number;
|
|
22
|
+
body_start_idx: number;
|
|
23
|
+
body_end_idx: number;
|
|
24
|
+
context: FoldContext;
|
|
25
|
+
right_start: number | null;
|
|
26
|
+
right_end: number | null;
|
|
27
|
+
left_start: number | null;
|
|
28
|
+
left_end: number | null;
|
|
29
|
+
// Identity of the definition the region snapped to; null for an
|
|
30
|
+
// indentation-fallback region.
|
|
31
|
+
qualified_name: string | null;
|
|
32
|
+
kind: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AttachedFold {
|
|
36
|
+
marker: SVGElement;
|
|
37
|
+
foldHandle: AnnotationHandle | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface FoldRequestAddress {
|
|
41
|
+
context: FoldContext;
|
|
42
|
+
right_start?: number;
|
|
43
|
+
right_end?: number;
|
|
44
|
+
left_start?: number;
|
|
45
|
+
left_end?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface FoldFileState {
|
|
49
|
+
handles: AnnotationHandle[];
|
|
50
|
+
chevrons: SVGElement[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const _FILE_FOLD_STATE: Record<string, FoldFileState> = Object.create(null);
|
|
54
|
+
|
|
55
|
+
function _sessionEndpoint(): string | null {
|
|
56
|
+
// Read at call time, not module init — the meta tag may be
|
|
57
|
+
// injected after this module loads (tests set up the DOM
|
|
58
|
+
// dynamically, and a future bootloader might too). Empty
|
|
59
|
+
// string content means "same origin" (the production case);
|
|
60
|
+
// a missing meta tag (null) means no server is available
|
|
61
|
+
// and the route is wired off.
|
|
62
|
+
if (typeof document === "undefined") return null;
|
|
63
|
+
const m = document.querySelector('meta[name="scr-session-endpoint"]');
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
return m.getAttribute("content") || "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- DOM helpers (private, duplicated from viewer.js because the
|
|
69
|
+
// classic-script module boundary doesn't let us import them) ----------
|
|
70
|
+
|
|
71
|
+
const _SVG_NS = "http://www.w3.org/2000/svg";
|
|
72
|
+
|
|
73
|
+
function _chev(folded: boolean, extraClass: string): SVGElement {
|
|
74
|
+
const svg = document.createElementNS(_SVG_NS, "svg") as unknown as SVGElement;
|
|
75
|
+
svg.setAttribute("viewBox", "0 0 12 12");
|
|
76
|
+
svg.setAttribute("aria-hidden", "true");
|
|
77
|
+
svg.classList.add("chevron");
|
|
78
|
+
svg.classList.add(extraClass);
|
|
79
|
+
if (!folded) svg.classList.add("open");
|
|
80
|
+
const path = document.createElementNS(_SVG_NS, "path");
|
|
81
|
+
path.setAttribute("d", "M4.25 2.75 L8 6 L4.25 9.25");
|
|
82
|
+
path.setAttribute("fill", "none");
|
|
83
|
+
path.setAttribute("stroke", "currentColor");
|
|
84
|
+
path.setAttribute("stroke-width", "1.75");
|
|
85
|
+
path.setAttribute("stroke-linecap", "round");
|
|
86
|
+
path.setAttribute("stroke-linejoin", "round");
|
|
87
|
+
svg.appendChild(path);
|
|
88
|
+
return svg;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _isRowContentEmpty(rowEl: HTMLElement | undefined | null): boolean {
|
|
92
|
+
if (!rowEl) return true;
|
|
93
|
+
const content = rowEl.children[1];
|
|
94
|
+
return !content || content.classList.contains("empty");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- File-level walker --------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function _teardownFileFolds(fileId: string): void {
|
|
100
|
+
const s = _FILE_FOLD_STATE[fileId];
|
|
101
|
+
if (!s) return;
|
|
102
|
+
for (const h of s.handles) {
|
|
103
|
+
try { h.remove(); } catch (_) { /* ignore */ }
|
|
104
|
+
}
|
|
105
|
+
for (const c of s.chevrons) {
|
|
106
|
+
try { c.remove(); } catch (_) { /* ignore */ }
|
|
107
|
+
}
|
|
108
|
+
delete _FILE_FOLD_STATE[fileId];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Walk the file body's .diff / .gap-expansion containers in DOM order,
|
|
112
|
+
// pull each one's row stream out of `FileRows` (recorded by render.ts
|
|
113
|
+
// at construction time), and flatten into one indexable list so folds
|
|
114
|
+
// can straddle hunks and adjacent gap-context.
|
|
115
|
+
function _collectFileRows(fileEl: HTMLElement): RowWithEls[] {
|
|
116
|
+
const body = fileEl.querySelector(".file-body");
|
|
117
|
+
if (!body) return [];
|
|
118
|
+
const out: RowWithEls[] = [];
|
|
119
|
+
for (const child of Array.from(body.children) as HTMLElement[]) {
|
|
120
|
+
const cls = child.classList;
|
|
121
|
+
let source: HTMLElement | null = null;
|
|
122
|
+
if (cls.contains("hunk")) {
|
|
123
|
+
source = child.querySelector(".diff");
|
|
124
|
+
} else if (cls.contains("gap-expansion")) {
|
|
125
|
+
source = child;
|
|
126
|
+
}
|
|
127
|
+
if (!source) continue;
|
|
128
|
+
const entry = FileRows.get(source);
|
|
129
|
+
if (!entry) continue;
|
|
130
|
+
for (let i = 0; i < entry.rows.length; i++) {
|
|
131
|
+
out.push({
|
|
132
|
+
...entry.rows[i],
|
|
133
|
+
oldEl: entry.oldEls[i], newEl: entry.newEls[i],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _findExistingFoldRecord(
|
|
141
|
+
file: FileBlock, det: DetectedRegion,
|
|
142
|
+
): FoldRegion | null {
|
|
143
|
+
const rs = det.right_start || 0, re_ = det.right_end || 0;
|
|
144
|
+
const ls = det.left_start || 0, le = det.left_end || 0;
|
|
145
|
+
for (const h of file.hunks || []) {
|
|
146
|
+
for (const r of h.fold_regions || []) {
|
|
147
|
+
if (
|
|
148
|
+
(r.context || "right") === det.context
|
|
149
|
+
&& (r.right_start || 0) === rs && (r.right_end || 0) === re_
|
|
150
|
+
&& (r.left_start || 0) === ls && (r.left_end || 0) === le
|
|
151
|
+
) {
|
|
152
|
+
return r;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _upsertFoldRegion(
|
|
160
|
+
file: FileBlock, det: DetectedRegion, rows: RowWithEls[],
|
|
161
|
+
): FoldRegion {
|
|
162
|
+
// The local POST handler and the SSE updater both mutate the
|
|
163
|
+
// region object's `summary` field — they need to point at the
|
|
164
|
+
// same reference. Find a matching persistent record if one
|
|
165
|
+
// exists, refresh its detected fields, and return it. Otherwise
|
|
166
|
+
// create a new one and stash it on the file's first hunk so the
|
|
167
|
+
// next render picks it up.
|
|
168
|
+
const hasChanges = _anyChangesInRange(rows, det.header_idx, det.body_end_idx);
|
|
169
|
+
const existing = _findExistingFoldRecord(file, det);
|
|
170
|
+
if (existing) {
|
|
171
|
+
existing.header_idx = det.header_idx;
|
|
172
|
+
existing.body_start_idx = det.body_start_idx;
|
|
173
|
+
existing.body_end_idx = det.body_end_idx;
|
|
174
|
+
existing.has_changes = hasChanges;
|
|
175
|
+
existing.qualified_name = det.qualified_name;
|
|
176
|
+
existing.kind = det.kind;
|
|
177
|
+
return existing;
|
|
178
|
+
}
|
|
179
|
+
const candidate: FoldRegion = {
|
|
180
|
+
header_idx: det.header_idx,
|
|
181
|
+
body_start_idx: det.body_start_idx,
|
|
182
|
+
body_end_idx: det.body_end_idx,
|
|
183
|
+
context: det.context,
|
|
184
|
+
right_start: det.right_start, right_end: det.right_end,
|
|
185
|
+
left_start: det.left_start, left_end: det.left_end,
|
|
186
|
+
has_changes: hasChanges,
|
|
187
|
+
qualified_name: det.qualified_name, kind: det.kind,
|
|
188
|
+
summary: "",
|
|
189
|
+
};
|
|
190
|
+
if (file.hunks && file.hunks.length > 0) {
|
|
191
|
+
const h0 = file.hunks[0];
|
|
192
|
+
if (!h0.fold_regions) h0.fold_regions = [];
|
|
193
|
+
h0.fold_regions.push(candidate);
|
|
194
|
+
}
|
|
195
|
+
return candidate;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _anyChangesInRange(
|
|
199
|
+
rows: RowWithEls[], start: number, end: number,
|
|
200
|
+
): boolean {
|
|
201
|
+
for (let i = start; i <= end; i++) {
|
|
202
|
+
const k = rows[i].kind;
|
|
203
|
+
if (k === "ins" || k === "del" || k === "pair") return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Indent-based detection --------------------------------------------
|
|
209
|
+
|
|
210
|
+
function _rowIndent(row: RowBlock): number {
|
|
211
|
+
const text = row.kind === "del" ? row.old_text : row.new_text;
|
|
212
|
+
if (!text || !text.trim()) return -1;
|
|
213
|
+
let ind = 0;
|
|
214
|
+
for (const ch of text) {
|
|
215
|
+
if (ch === " ") ind += 1;
|
|
216
|
+
else if (ch === "\t") ind += 4;
|
|
217
|
+
else break;
|
|
218
|
+
}
|
|
219
|
+
return ind;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _indentRawRegions(rows: RowWithEls[]): Array<[number, number]> {
|
|
223
|
+
const indents = rows.map(_rowIndent);
|
|
224
|
+
const nextNonBlank = (i: number): number | null => {
|
|
225
|
+
for (let j = i + 1; j < indents.length; j++) {
|
|
226
|
+
if (indents[j] !== -1) return indents[j];
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
};
|
|
230
|
+
const raw: Array<[number, number]> = [];
|
|
231
|
+
const stack: Array<[number, number]> = [];
|
|
232
|
+
for (let i = 0; i < indents.length; i++) {
|
|
233
|
+
const ind = indents[i];
|
|
234
|
+
if (ind === -1) continue;
|
|
235
|
+
while (stack.length && stack[stack.length - 1][0] >= ind) {
|
|
236
|
+
const top = stack.pop()!;
|
|
237
|
+
raw.push([top[1], i - 1]);
|
|
238
|
+
}
|
|
239
|
+
const ni = nextNonBlank(i);
|
|
240
|
+
if (ni !== null && ni > ind) stack.push([ind, i]);
|
|
241
|
+
}
|
|
242
|
+
while (stack.length) {
|
|
243
|
+
const top = stack.pop()!;
|
|
244
|
+
raw.push([top[1], indents.length - 1]);
|
|
245
|
+
}
|
|
246
|
+
return raw;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Definition spans enclosing a row, outermost-first: the row maps by
|
|
250
|
+
// line number into one side's tree — new_line into head spans (ctx /
|
|
251
|
+
// pair / ins rows), else old_line into base spans (del-only rows).
|
|
252
|
+
function _rowSymbols(
|
|
253
|
+
row: RowWithEls, headSpans: FoldSymbolSpan[], baseSpans: FoldSymbolSpan[],
|
|
254
|
+
): FoldSymbolSpan[] {
|
|
255
|
+
let line: number | null;
|
|
256
|
+
let spans: FoldSymbolSpan[];
|
|
257
|
+
if (row.new_line != null) { line = row.new_line; spans = headSpans; }
|
|
258
|
+
else if (row.old_line != null) { line = row.old_line; spans = baseSpans; }
|
|
259
|
+
else return [];
|
|
260
|
+
return spans
|
|
261
|
+
.filter((s) => s.start_line <= line! && line! <= s.end_line)
|
|
262
|
+
.sort((a, b) => a.depth - b.depth);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// `(headerIdx, bodyEndIdx, qualifiedName, kind)` snapped to definition
|
|
266
|
+
// spans, plus the set of row indices inside any definition. Every
|
|
267
|
+
// definition with >=1 present row becomes a region from its first to its
|
|
268
|
+
// last present row carrying that definition's identity; nested defs nest
|
|
269
|
+
// because a row carries its whole enclosing chain.
|
|
270
|
+
type RawRegion = [number, number, string | null, string | null];
|
|
271
|
+
|
|
272
|
+
function _symbolRawRegions(
|
|
273
|
+
rows: RowWithEls[], headSpans: FoldSymbolSpan[], baseSpans: FoldSymbolSpan[],
|
|
274
|
+
): { raw: RawRegion[]; covered: Set<number> } {
|
|
275
|
+
const runs = new Map<string, [number, number]>();
|
|
276
|
+
const kinds = new Map<string, string>();
|
|
277
|
+
const order: string[] = [];
|
|
278
|
+
const covered = new Set<number>();
|
|
279
|
+
for (let i = 0; i < rows.length; i++) {
|
|
280
|
+
for (const s of _rowSymbols(rows[i], headSpans, baseSpans)) {
|
|
281
|
+
covered.add(i);
|
|
282
|
+
const run = runs.get(s.qualified_name);
|
|
283
|
+
if (run === undefined) {
|
|
284
|
+
runs.set(s.qualified_name, [i, i]);
|
|
285
|
+
kinds.set(s.qualified_name, s.kind);
|
|
286
|
+
order.push(s.qualified_name);
|
|
287
|
+
} else {
|
|
288
|
+
run[1] = i;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const raw: RawRegion[] = order.map((qn) => {
|
|
293
|
+
const run = runs.get(qn)!;
|
|
294
|
+
return [run[0], run[1], qn, kinds.get(qn)!];
|
|
295
|
+
});
|
|
296
|
+
return { raw, covered };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function _computeFoldRegions(
|
|
300
|
+
rows: RowWithEls[],
|
|
301
|
+
headSpans: FoldSymbolSpan[] = [],
|
|
302
|
+
baseSpans: FoldSymbolSpan[] = [],
|
|
303
|
+
): DetectedRegion[] {
|
|
304
|
+
// Uniform shape: [headerIdx, bodyEndIdx, qualifiedName|null, kind|null].
|
|
305
|
+
// Indentation regions carry no symbol.
|
|
306
|
+
let raw: RawRegion[];
|
|
307
|
+
if (headSpans.length || baseSpans.length) {
|
|
308
|
+
const sym = _symbolRawRegions(rows, headSpans, baseSpans);
|
|
309
|
+
// Keep an indentation region only where no row it spans is already
|
|
310
|
+
// covered by a definition — the snapped region owns that stretch.
|
|
311
|
+
raw = sym.raw.concat(
|
|
312
|
+
_indentRawRegions(rows)
|
|
313
|
+
.filter(([h, e]) => {
|
|
314
|
+
for (let j = h; j <= e; j++) if (sym.covered.has(j)) return false;
|
|
315
|
+
return true;
|
|
316
|
+
})
|
|
317
|
+
.map(([h, e]): RawRegion => [h, e, null, null]),
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
raw = _indentRawRegions(rows).map(([h, e]): RawRegion => [h, e, null, null]);
|
|
321
|
+
}
|
|
322
|
+
raw.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
323
|
+
const regions: DetectedRegion[] = [];
|
|
324
|
+
for (const [header_idx, body_end, qualified_name, kind] of raw) {
|
|
325
|
+
const body_start = header_idx + 1;
|
|
326
|
+
if (body_start > body_end) continue;
|
|
327
|
+
const right_start = _firstLine(rows, header_idx, body_end, "new_line");
|
|
328
|
+
const right_end = _lastLine(rows, header_idx, body_end, "new_line");
|
|
329
|
+
const left_start = _firstLine(rows, header_idx, body_end, "old_line");
|
|
330
|
+
const left_end = _lastLine(rows, header_idx, body_end, "old_line");
|
|
331
|
+
const hasChanges = _anyChangesInRange(rows, header_idx, body_end);
|
|
332
|
+
let context: FoldContext;
|
|
333
|
+
if (right_start != null && left_start != null && hasChanges) context = "both";
|
|
334
|
+
else if (right_start != null) context = "right";
|
|
335
|
+
else context = "left";
|
|
336
|
+
regions.push({
|
|
337
|
+
header_idx, body_start_idx: body_start, body_end_idx: body_end,
|
|
338
|
+
context, right_start, right_end, left_start, left_end,
|
|
339
|
+
qualified_name, kind,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return regions;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function _firstLine(
|
|
346
|
+
rows: RowWithEls[], start: number, end: number, attr: "new_line" | "old_line",
|
|
347
|
+
): number | null {
|
|
348
|
+
for (let j = start; j <= end; j++) {
|
|
349
|
+
const v = rows[j][attr];
|
|
350
|
+
if (v != null) return v;
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function _lastLine(
|
|
356
|
+
rows: RowWithEls[], start: number, end: number, attr: "new_line" | "old_line",
|
|
357
|
+
): number | null {
|
|
358
|
+
for (let j = end; j >= start; j--) {
|
|
359
|
+
const v = rows[j][attr];
|
|
360
|
+
if (v != null) return v;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- Attach + click ----------------------------------------------------
|
|
366
|
+
|
|
367
|
+
function _canRequestFoldSummary(
|
|
368
|
+
fileIdx: number | null, region: FoldRegion,
|
|
369
|
+
): boolean {
|
|
370
|
+
if (_sessionEndpoint() === null) return false;
|
|
371
|
+
if (fileIdx == null) return false;
|
|
372
|
+
return _foldAddress(region) !== null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function _foldAddress(region: FoldRegion): FoldRequestAddress | null {
|
|
376
|
+
const context = region.context || "right";
|
|
377
|
+
const addr: FoldRequestAddress = { context };
|
|
378
|
+
if (context === "right" || context === "both") {
|
|
379
|
+
if (region.right_start == null || region.right_end == null) return null;
|
|
380
|
+
addr.right_start = region.right_start;
|
|
381
|
+
addr.right_end = region.right_end;
|
|
382
|
+
}
|
|
383
|
+
if (context === "left" || context === "both") {
|
|
384
|
+
if (region.left_start == null || region.left_end == null) return null;
|
|
385
|
+
addr.left_start = region.left_start;
|
|
386
|
+
addr.left_end = region.left_end;
|
|
387
|
+
}
|
|
388
|
+
return addr;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Prefix the collapsed placeholder with the region's symbol identity,
|
|
392
|
+
// e.g. "function Foo.bar — ". Empty for an indentation-fallback region
|
|
393
|
+
// (no symbol), which keeps today's unlabelled placeholder.
|
|
394
|
+
function _foldLabel(region: FoldRegion): string {
|
|
395
|
+
if (!region.qualified_name) return "";
|
|
396
|
+
const kind = region.kind ? `${region.kind} ` : "";
|
|
397
|
+
return `${kind}${region.qualified_name} — `;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function _requestFoldSummary(
|
|
401
|
+
fileIdx: number, region: FoldRegion,
|
|
402
|
+
foldHandle: AnnotationHandle,
|
|
403
|
+
): void {
|
|
404
|
+
if (region._inflight || region.summary) return;
|
|
405
|
+
const addr = _foldAddress(region);
|
|
406
|
+
if (!addr) return;
|
|
407
|
+
region._inflight = true;
|
|
408
|
+
const label = _foldLabel(region);
|
|
409
|
+
_setFoldBoxContent(foldHandle, label + "summarising…", { pending: true });
|
|
410
|
+
const retry = (): void => _requestFoldSummary(fileIdx, region, foldHandle);
|
|
411
|
+
fetch(_sessionEndpoint() + "/fold-summary", {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers: { "Content-Type": "application/json" },
|
|
414
|
+
body: JSON.stringify({ file_idx: fileIdx, ...addr }),
|
|
415
|
+
})
|
|
416
|
+
.then((r) => r.json().then((j: { summary?: string }) => ({ status: r.status, body: j })))
|
|
417
|
+
.then(({ status, body }) => {
|
|
418
|
+
region._inflight = false;
|
|
419
|
+
if (status === 200 && body.summary) {
|
|
420
|
+
region.summary = body.summary;
|
|
421
|
+
_setFoldBoxContent(foldHandle, label + body.summary, {});
|
|
422
|
+
} else {
|
|
423
|
+
_setFoldBoxContent(
|
|
424
|
+
foldHandle, label + "(summary failed — click to retry)",
|
|
425
|
+
{ failed: true }, retry,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
.catch(() => {
|
|
430
|
+
region._inflight = false;
|
|
431
|
+
_setFoldBoxContent(
|
|
432
|
+
foldHandle, label + "(summary failed — click to retry)",
|
|
433
|
+
{ failed: true }, retry,
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function _setFoldBoxContent(
|
|
439
|
+
foldHandle: AnnotationHandle, text: string,
|
|
440
|
+
classes: { pending?: boolean; failed?: boolean },
|
|
441
|
+
onClick?: () => void,
|
|
442
|
+
): void {
|
|
443
|
+
if (!foldHandle || !foldHandle.element) return;
|
|
444
|
+
const box = foldHandle.element.querySelector(".annot-box") as HTMLElement | null;
|
|
445
|
+
if (!box) return;
|
|
446
|
+
box.textContent = text;
|
|
447
|
+
box.classList.remove("pending", "failed");
|
|
448
|
+
if (classes.pending) box.classList.add("pending");
|
|
449
|
+
if (classes.failed) box.classList.add("failed");
|
|
450
|
+
if (onClick) {
|
|
451
|
+
const clone = box.cloneNode(true) as HTMLElement;
|
|
452
|
+
clone.style.cursor = "pointer";
|
|
453
|
+
clone.addEventListener("click", onClick);
|
|
454
|
+
box.replaceWith(clone);
|
|
455
|
+
}
|
|
456
|
+
foldHandle.resize();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function _attachOneFold(
|
|
460
|
+
rows: RowWithEls[], region: FoldRegion, fileIdx: number,
|
|
461
|
+
): AttachedFold | null {
|
|
462
|
+
const bodyStart = region.body_start_idx;
|
|
463
|
+
const bodyEnd = region.body_end_idx;
|
|
464
|
+
if (bodyStart > bodyEnd) return null;
|
|
465
|
+
|
|
466
|
+
const headerRow = rows[region.header_idx];
|
|
467
|
+
if (!headerRow) return null;
|
|
468
|
+
const headerOld = headerRow.oldEl;
|
|
469
|
+
const headerNew = headerRow.newEl;
|
|
470
|
+
if (!headerOld && !headerNew) return null;
|
|
471
|
+
|
|
472
|
+
const side = _isRowContentEmpty(headerNew) && !_isRowContentEmpty(headerOld)
|
|
473
|
+
? "old" : "new";
|
|
474
|
+
const anchor = side === "new" ? headerNew : headerOld;
|
|
475
|
+
const shadow = side === "new" ? headerOld : headerNew;
|
|
476
|
+
|
|
477
|
+
const marker = _chev(false, "fold-chev");
|
|
478
|
+
marker.setAttribute("role", "button");
|
|
479
|
+
marker.setAttribute("tabindex", "0");
|
|
480
|
+
|
|
481
|
+
let foldHandle: AnnotationHandle | null = null;
|
|
482
|
+
const canSummarise = _canRequestFoldSummary(fileIdx, region);
|
|
483
|
+
if (region.summary || region.has_changes || canSummarise) {
|
|
484
|
+
// Seed the placeholder with the symbol identity (if any) followed by
|
|
485
|
+
// the summary or its pending/run-augment stand-in.
|
|
486
|
+
const label = _foldLabel(region);
|
|
487
|
+
const pending = !region.summary && canSummarise;
|
|
488
|
+
const bodyText = region.summary
|
|
489
|
+
|| (canSummarise
|
|
490
|
+
? "summarising…"
|
|
491
|
+
: "(changes here; run augment to generate a description)");
|
|
492
|
+
foldHandle = Annotations.attach({
|
|
493
|
+
anchor, shadowAnchor: shadow,
|
|
494
|
+
variant: "fold", content: label + bodyText,
|
|
495
|
+
});
|
|
496
|
+
if (!region.summary) {
|
|
497
|
+
const box = foldHandle.element.querySelector(".annot-box");
|
|
498
|
+
if (box) box.classList.add("missing");
|
|
499
|
+
if (pending && box) box.classList.add("pending");
|
|
500
|
+
}
|
|
501
|
+
foldHandle.element.style.display = "none";
|
|
502
|
+
if (foldHandle.placeholder) foldHandle.placeholder.style.display = "none";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
marker.addEventListener("click", (e) => {
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
const nowOpen = marker.classList.toggle("open");
|
|
508
|
+
for (let i = bodyStart; i <= bodyEnd; i++) {
|
|
509
|
+
const r = rows[i];
|
|
510
|
+
if (!r) continue;
|
|
511
|
+
if (r.oldEl) r.oldEl.style.display = nowOpen ? "" : "none";
|
|
512
|
+
if (r.newEl) r.newEl.style.display = nowOpen ? "" : "none";
|
|
513
|
+
}
|
|
514
|
+
if (foldHandle) {
|
|
515
|
+
foldHandle.element.style.display = nowOpen ? "none" : "";
|
|
516
|
+
if (foldHandle.placeholder) {
|
|
517
|
+
foldHandle.placeholder.style.display = nowOpen ? "none" : "";
|
|
518
|
+
}
|
|
519
|
+
if (!nowOpen) foldHandle.resize();
|
|
520
|
+
}
|
|
521
|
+
if (!nowOpen && !region.summary && foldHandle
|
|
522
|
+
&& _canRequestFoldSummary(fileIdx, region)) {
|
|
523
|
+
_requestFoldSummary(fileIdx, region, foldHandle);
|
|
524
|
+
}
|
|
525
|
+
Annotations.reflow(anchor);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const contentCell = anchor && (anchor.children[1] as HTMLElement | undefined);
|
|
529
|
+
if (contentCell) contentCell.prepend(marker);
|
|
530
|
+
return { marker, foldHandle };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function attachFileFolds(fileEl: HTMLElement, file: FileBlock): void {
|
|
534
|
+
_teardownFileFolds(file.id);
|
|
535
|
+
const fileIdx = Number(file.id.replace("F", ""));
|
|
536
|
+
const rows = _collectFileRows(fileEl);
|
|
537
|
+
if (rows.length === 0) return;
|
|
538
|
+
const syms = file.fold_symbols || { head: [], base: [] };
|
|
539
|
+
const detected = _computeFoldRegions(rows, syms.head, syms.base);
|
|
540
|
+
const handles: AnnotationHandle[] = [];
|
|
541
|
+
const chevrons: SVGElement[] = [];
|
|
542
|
+
for (const det of detected) {
|
|
543
|
+
const region = _upsertFoldRegion(file, det, rows);
|
|
544
|
+
const attached = _attachOneFold(rows, region, fileIdx);
|
|
545
|
+
if (!attached) continue;
|
|
546
|
+
if (attached.foldHandle) handles.push(attached.foldHandle);
|
|
547
|
+
if (attached.marker) chevrons.push(attached.marker);
|
|
548
|
+
}
|
|
549
|
+
_FILE_FOLD_STATE[file.id] = { handles, chevrons };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// The single runtime surface. boot.ts calls attachFileFolds on
|
|
553
|
+
// initial render, after every gap expand/collapse, and from
|
|
554
|
+
// applyFoldSummary's cross-tab path.
|
|
555
|
+
export const Folds = { attachFileFolds };
|
|
556
|
+
|
|
557
|
+
// Exposed for the cross-language lockstep fixture (tests/js/folds.test.ts):
|
|
558
|
+
// the same (rows, spans) input must yield the same regions as the Python
|
|
559
|
+
// `compute_fold_regions`. Not used by the runtime bundle.
|
|
560
|
+
export { _computeFoldRegions };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="scr-session-endpoint" content="">
|
|
7
|
+
<title>Semantic Code Review</title>
|
|
8
|
+
<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="/static/vendor/github-dark.min.css">
|
|
9
|
+
<link rel="stylesheet" media="(prefers-color-scheme: light)" href="/static/vendor/github.min.css">
|
|
10
|
+
<link rel="stylesheet" href="/static/viewer.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<header class="pr-bar">
|
|
14
|
+
<div class="pr-title">
|
|
15
|
+
<span class="pr-meta"></span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="fold-slider" role="tablist" aria-label="Fold level">
|
|
18
|
+
<button data-fold="files" title="Collapse to files (1)">Files</button>
|
|
19
|
+
<button data-fold="hunks" title="Collapse to hunks (2)">Hunks</button>
|
|
20
|
+
<button data-fold="segments" title="Collapse to segments (3)">Segments</button>
|
|
21
|
+
<button data-fold="off" title="Fully expanded (4)">Off</button>
|
|
22
|
+
</div>
|
|
23
|
+
<button id="reset-btn" class="reset-btn" title="Clear per-region overrides">Reset</button>
|
|
24
|
+
<button id="help-btn" class="help-btn" title="Help (?)" aria-label="Help">?</button>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<div id="scr-progress" class="scr-progress hidden" aria-live="polite">
|
|
28
|
+
<div class="scr-progress-summary">
|
|
29
|
+
<span class="scr-progress-overview" data-state="pending">Overview</span>
|
|
30
|
+
<span class="scr-progress-hunks">Hunks <span class="scr-progress-done">0</span>/<span class="scr-progress-total">0</span></span>
|
|
31
|
+
<span class="scr-progress-detail">
|
|
32
|
+
(<span class="scr-progress-running">0</span> running ·
|
|
33
|
+
<span class="scr-progress-queued">0</span> queued ·
|
|
34
|
+
<span class="scr-progress-failed">0</span> failed)
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="scr-progress-grid" role="list" aria-label="Hunk progress"></div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="layout">
|
|
41
|
+
<aside id="group-sidebar" class="group-sidebar"></aside>
|
|
42
|
+
<main id="app"></main>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<footer id="status-bar" class="status-bar">
|
|
46
|
+
<span id="status-counts" class="status-counts"></span>
|
|
47
|
+
</footer>
|
|
48
|
+
|
|
49
|
+
<div id="help-overlay" class="help-overlay hidden" role="dialog" aria-label="Help">
|
|
50
|
+
<div class="help-card">
|
|
51
|
+
<h3>Keyboard shortcuts</h3>
|
|
52
|
+
<p><kbd>1</kbd> fold to files · <kbd>2</kbd> hunks · <kbd>3</kbd> segments · <kbd>4</kbd> expand</p>
|
|
53
|
+
<p><kbd>?</kbd> toggle this help · <kbd>Esc</kbd> close</p>
|
|
54
|
+
<p>Click any region header to toggle its fold state independently.
|
|
55
|
+
"Reset" clears per-region overrides and applies the global fold level.</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<script src="/static/vendor/highlight.min.js"></script>
|
|
60
|
+
<script src="/static/viewer.js"></script>
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|