@jonathan0823/pi-sonarqube 0.4.0 → 0.6.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 +2 -2
- package/dist/api.d.ts +12 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +116 -26
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +2 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +285 -30
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +182 -52
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -4
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +29 -13
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +160 -70
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +310 -59
- package/src/commands.ts +454 -66
- package/src/config.ts +84 -23
- package/src/index.ts +462 -94
- package/src/types.ts +29 -4
- package/src/ui.ts +271 -85
package/src/types.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface FileDuplication {
|
|
|
41
41
|
filePath: string;
|
|
42
42
|
fileKey: string;
|
|
43
43
|
duplicatedLines: number;
|
|
44
|
+
duplicatedLinesDensity: number;
|
|
44
45
|
duplicatedBlocks: number;
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -85,6 +86,8 @@ export interface SonarIssueFetchOptions {
|
|
|
85
86
|
rules?: string[];
|
|
86
87
|
softwareQualities?: string[];
|
|
87
88
|
impactSeverities?: string[];
|
|
89
|
+
pathScope?: string;
|
|
90
|
+
componentKeys?: string[];
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
export interface SonarInitConfig {
|
|
@@ -119,8 +122,30 @@ export interface InitCommandOptions {
|
|
|
119
122
|
|
|
120
123
|
// ── Constants shared by adapter and extension ───────────────────────────────
|
|
121
124
|
|
|
122
|
-
export const SONAR_SEVERITIES = [
|
|
123
|
-
|
|
125
|
+
export const SONAR_SEVERITIES = [
|
|
126
|
+
"BLOCKER",
|
|
127
|
+
"CRITICAL",
|
|
128
|
+
"MAJOR",
|
|
129
|
+
"MINOR",
|
|
130
|
+
"INFO",
|
|
131
|
+
] as const;
|
|
132
|
+
export const SONAR_STATUSES = [
|
|
133
|
+
"OPEN",
|
|
134
|
+
"CONFIRMED",
|
|
135
|
+
"REOPENED",
|
|
136
|
+
"RESOLVED",
|
|
137
|
+
"CLOSED",
|
|
138
|
+
] as const;
|
|
124
139
|
export const SONAR_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"] as const;
|
|
125
|
-
export const SONAR_SOFTWARE_QUALITIES = [
|
|
126
|
-
|
|
140
|
+
export const SONAR_SOFTWARE_QUALITIES = [
|
|
141
|
+
"MAINTAINABILITY",
|
|
142
|
+
"RELIABILITY",
|
|
143
|
+
"SECURITY",
|
|
144
|
+
] as const;
|
|
145
|
+
export const SONAR_IMPACT_SEVERITIES = [
|
|
146
|
+
"BLOCKER",
|
|
147
|
+
"HIGH",
|
|
148
|
+
"MEDIUM",
|
|
149
|
+
"LOW",
|
|
150
|
+
"INFO",
|
|
151
|
+
] as const;
|
package/src/ui.ts
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
Theme,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ExtensionCommandContext,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Input, Key, matchesKey, truncateToWidth, type Focusable } from "@earendil-works/pi-tui";
|
|
3
7
|
import { readFile } from "node:fs/promises";
|
|
4
8
|
import { resolve } from "node:path";
|
|
5
|
-
import type {
|
|
6
|
-
|
|
9
|
+
import type {
|
|
10
|
+
SonarIssue,
|
|
11
|
+
SonarAnalysisState,
|
|
12
|
+
SonarIssueFetchOptions,
|
|
13
|
+
FileDuplication,
|
|
14
|
+
DuplicationBlockGroup,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
import {
|
|
17
|
+
issueFilterLabel,
|
|
18
|
+
fetchIssues,
|
|
19
|
+
createAnalysisState,
|
|
20
|
+
normalizeIssueFilters,
|
|
21
|
+
fetchCleanCodeMode,
|
|
22
|
+
} from "./api.js";
|
|
7
23
|
import { resolveConfig, resolveTarget } from "./config.js";
|
|
8
24
|
|
|
9
25
|
// ── Issue preview ───────────────────────────────────────────────────────────
|
|
10
26
|
|
|
11
|
-
export async function buildIssuePreview(
|
|
27
|
+
export async function buildIssuePreview(
|
|
28
|
+
baseDir: string,
|
|
29
|
+
issue: SonarIssue,
|
|
30
|
+
radius = 3,
|
|
31
|
+
): Promise<string> {
|
|
12
32
|
const filePath = resolve(baseDir, issue.filePath);
|
|
13
33
|
const content = await readFile(filePath, "utf8");
|
|
14
34
|
const lines = content.split(/\r?\n/);
|
|
@@ -40,7 +60,11 @@ export async function buildDuplicationPreview(
|
|
|
40
60
|
cache.set(absolutePath, content);
|
|
41
61
|
return content.split(/\r?\n/);
|
|
42
62
|
};
|
|
43
|
-
const renderRange = (
|
|
63
|
+
const renderRange = (
|
|
64
|
+
lines: string[],
|
|
65
|
+
from: number,
|
|
66
|
+
size: number,
|
|
67
|
+
): string[] => {
|
|
44
68
|
const start = Math.max(1, from);
|
|
45
69
|
const end = Math.min(lines.length, from + size - 1);
|
|
46
70
|
const width = String(end).length;
|
|
@@ -76,7 +100,18 @@ export async function buildDuplicationPreview(
|
|
|
76
100
|
|
|
77
101
|
// ── Analysis UI ─────────────────────────────────────────────────────────────
|
|
78
102
|
|
|
79
|
-
const ANALYSIS_SPINNER = [
|
|
103
|
+
const ANALYSIS_SPINNER = [
|
|
104
|
+
"⠋",
|
|
105
|
+
"⠙",
|
|
106
|
+
"⠹",
|
|
107
|
+
"⠸",
|
|
108
|
+
"⠼",
|
|
109
|
+
"⠴",
|
|
110
|
+
"⠦",
|
|
111
|
+
"⠧",
|
|
112
|
+
"⠇",
|
|
113
|
+
"⠏",
|
|
114
|
+
] as const;
|
|
80
115
|
const ANALYSIS_WIDGET_KEY = "sonarqube-analysis";
|
|
81
116
|
|
|
82
117
|
export interface AnalysisUiHandle {
|
|
@@ -84,16 +119,25 @@ export interface AnalysisUiHandle {
|
|
|
84
119
|
stop(): void;
|
|
85
120
|
}
|
|
86
121
|
|
|
87
|
-
export function startAnalysisUi(
|
|
122
|
+
export function startAnalysisUi(
|
|
123
|
+
ctx: ExtensionContext,
|
|
124
|
+
projectKey: string,
|
|
125
|
+
): AnalysisUiHandle {
|
|
88
126
|
let frameIndex = 0;
|
|
89
127
|
let phase = "Starting...";
|
|
90
128
|
const render = () => {
|
|
91
129
|
const frame = ANALYSIS_SPINNER[frameIndex];
|
|
92
|
-
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, [
|
|
130
|
+
ctx.ui.setWidget(ANALYSIS_WIDGET_KEY, [
|
|
131
|
+
`${frame} SonarQube ${projectKey}`,
|
|
132
|
+
phase,
|
|
133
|
+
]);
|
|
93
134
|
};
|
|
94
135
|
|
|
95
136
|
ctx.ui.setWorkingMessage(`Analyzing ${projectKey}...`);
|
|
96
|
-
ctx.ui.setWorkingIndicator({
|
|
137
|
+
ctx.ui.setWorkingIndicator({
|
|
138
|
+
frames: ANALYSIS_SPINNER as unknown as string[],
|
|
139
|
+
intervalMs: 80,
|
|
140
|
+
});
|
|
97
141
|
render();
|
|
98
142
|
|
|
99
143
|
const timer = setInterval(() => {
|
|
@@ -115,46 +159,177 @@ export function startAnalysisUi(ctx: ExtensionContext, projectKey: string): Anal
|
|
|
115
159
|
};
|
|
116
160
|
}
|
|
117
161
|
|
|
118
|
-
// ──
|
|
162
|
+
// ── Searchable browser helpers ─────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function normalizeSearchQuery(query: string): string[] {
|
|
165
|
+
return query
|
|
166
|
+
.trim()
|
|
167
|
+
.toLowerCase()
|
|
168
|
+
.split(/\s+/)
|
|
169
|
+
.filter(Boolean);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function matchesSearchQuery(haystack: string, query: string): boolean {
|
|
173
|
+
const tokens = normalizeSearchQuery(query);
|
|
174
|
+
if (tokens.length === 0) return true;
|
|
175
|
+
const text = haystack.toLowerCase();
|
|
176
|
+
return tokens.every((token) => text.includes(token));
|
|
177
|
+
}
|
|
119
178
|
|
|
120
|
-
|
|
179
|
+
function filterSearchItems<T>(
|
|
180
|
+
items: readonly T[],
|
|
181
|
+
query: string,
|
|
182
|
+
searchText: (item: T) => string,
|
|
183
|
+
): T[] {
|
|
184
|
+
if (!query.trim()) return [...items];
|
|
185
|
+
return items.filter((item) => matchesSearchQuery(searchText(item), query));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface SearchableBrowserConfig<T> {
|
|
189
|
+
title: string;
|
|
190
|
+
subtitle: (query: string, totalCount: number, filteredCount: number) => string;
|
|
191
|
+
searchHint: string;
|
|
192
|
+
emptyMessage: string;
|
|
193
|
+
footer: string;
|
|
194
|
+
pageSize: number;
|
|
195
|
+
searchText: (item: T) => string;
|
|
196
|
+
renderItem: (item: T, index: number, isSelected: boolean) => string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class SearchableListBrowser<T> implements Focusable {
|
|
121
200
|
private selected = 0;
|
|
201
|
+
private query = "";
|
|
202
|
+
private filteredItems: T[];
|
|
203
|
+
private _focused = false;
|
|
204
|
+
private readonly searchInput = new Input();
|
|
122
205
|
|
|
123
206
|
constructor(
|
|
124
|
-
private readonly
|
|
207
|
+
private readonly items: readonly T[],
|
|
125
208
|
private readonly theme: Theme,
|
|
126
209
|
private readonly done: (result: number | null) => void,
|
|
127
|
-
|
|
210
|
+
private readonly config: SearchableBrowserConfig<T>,
|
|
211
|
+
) {
|
|
212
|
+
this.filteredItems = [...items];
|
|
213
|
+
}
|
|
128
214
|
|
|
129
|
-
|
|
215
|
+
get focused(): boolean {
|
|
216
|
+
return this._focused;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
set focused(value: boolean) {
|
|
220
|
+
this._focused = value;
|
|
221
|
+
this.searchInput.focused = value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
invalidate(): void {
|
|
225
|
+
this.searchInput.invalidate();
|
|
226
|
+
}
|
|
130
227
|
|
|
131
228
|
handleInput(data: string): void {
|
|
132
|
-
|
|
229
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
230
|
+
this.done(null);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (matchesKey(data, Key.up)) {
|
|
235
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (matchesKey(data, Key.down)) {
|
|
240
|
+
this.selected = Math.min(
|
|
241
|
+
Math.max(0, this.filteredItems.length - 1),
|
|
242
|
+
this.selected + 1,
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (matchesKey(data, Key.enter)) {
|
|
248
|
+
if (this.filteredItems.length > 0) this.done(this.selected);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.searchInput.handleInput(data);
|
|
253
|
+
this.syncFilter();
|
|
133
254
|
}
|
|
134
255
|
|
|
135
256
|
render(width: number): string[] {
|
|
136
|
-
const
|
|
137
|
-
|
|
257
|
+
const subtitle = this.config.subtitle(
|
|
258
|
+
this.query,
|
|
259
|
+
this.items.length,
|
|
260
|
+
this.filteredItems.length,
|
|
261
|
+
);
|
|
262
|
+
return [
|
|
263
|
+
truncateToWidth(this.theme.fg("dim", this.config.searchHint), width),
|
|
264
|
+
...this.searchInput.render(width),
|
|
265
|
+
"",
|
|
266
|
+
...renderListBrowser(this.theme, width, {
|
|
267
|
+
title: this.config.title,
|
|
268
|
+
subtitle,
|
|
269
|
+
items: this.filteredItems,
|
|
270
|
+
selected: this.selected,
|
|
271
|
+
pageSize: this.config.pageSize,
|
|
272
|
+
emptyMessage: this.config.emptyMessage,
|
|
273
|
+
footer: this.config.footer,
|
|
274
|
+
renderItem: this.config.renderItem,
|
|
275
|
+
}),
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private syncFilter(): void {
|
|
280
|
+
const nextQuery = this.searchInput.getValue();
|
|
281
|
+
if (nextQuery === this.query) return;
|
|
282
|
+
this.query = nextQuery;
|
|
283
|
+
this.filteredItems = filterSearchItems(
|
|
284
|
+
this.items,
|
|
285
|
+
nextQuery,
|
|
286
|
+
this.config.searchText,
|
|
287
|
+
);
|
|
288
|
+
this.selected = 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export class IssueBrowser extends SearchableListBrowser<SonarIssue> {
|
|
293
|
+
constructor(
|
|
294
|
+
state: SonarAnalysisState,
|
|
295
|
+
theme: Theme,
|
|
296
|
+
done: (result: number | null) => void,
|
|
297
|
+
) {
|
|
298
|
+
const filterSuffix = state.filters ? ` • ${issueFilterLabel(state.filters)}` : "";
|
|
299
|
+
super(state.issues, theme, done, {
|
|
138
300
|
title: "SonarQube Issues",
|
|
139
|
-
subtitle:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
emptyMessage: "No open issues found.",
|
|
301
|
+
subtitle: (_query, totalCount, filteredCount) =>
|
|
302
|
+
`${state.projectKey} • ${totalCount} issue(s) • ${filteredCount} match(es)${filterSuffix}`,
|
|
303
|
+
searchHint: "Search issues by file, rule, severity, status, or message",
|
|
304
|
+
emptyMessage: "No matching issues found.",
|
|
144
305
|
footer: "Up/Down to move, Enter to preview, Esc to close",
|
|
306
|
+
pageSize: 20,
|
|
307
|
+
searchText: (issue) =>
|
|
308
|
+
[
|
|
309
|
+
issue.filePath,
|
|
310
|
+
issue.rule,
|
|
311
|
+
issue.ruleName ?? "",
|
|
312
|
+
issue.severity,
|
|
313
|
+
issue.status ?? "",
|
|
314
|
+
issue.message,
|
|
315
|
+
].join(" "),
|
|
145
316
|
renderItem: (issue, issueIndex, isSelected) => {
|
|
146
|
-
const marker = isSelected
|
|
317
|
+
const marker = isSelected
|
|
318
|
+
? theme.fg("accent", ">")
|
|
319
|
+
: theme.fg("dim", " ");
|
|
147
320
|
const location = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
148
|
-
const rule = issue.ruleName
|
|
149
|
-
|
|
321
|
+
const rule = issue.ruleName
|
|
322
|
+
? `${issue.rule} (${issue.ruleName})`
|
|
323
|
+
: issue.rule;
|
|
324
|
+
const severity = severityColor(theme, issue.severity);
|
|
150
325
|
return [
|
|
151
326
|
marker,
|
|
152
327
|
String(issueIndex + 1).padStart(2, " "),
|
|
153
328
|
".",
|
|
154
329
|
severity,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
330
|
+
theme.fg("accent", location),
|
|
331
|
+
theme.fg("muted", rule),
|
|
332
|
+
theme.fg("text", `— ${issue.message}`),
|
|
158
333
|
].join(" ");
|
|
159
334
|
},
|
|
160
335
|
});
|
|
@@ -177,29 +352,11 @@ function severityColor(theme: Theme, severity: string): string {
|
|
|
177
352
|
}
|
|
178
353
|
}
|
|
179
354
|
|
|
180
|
-
function
|
|
181
|
-
data: string,
|
|
355
|
+
function getBrowserWindow(
|
|
182
356
|
selected: number,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
): number {
|
|
186
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
187
|
-
done(null);
|
|
188
|
-
return selected;
|
|
189
|
-
}
|
|
190
|
-
if (matchesKey(data, Key.up)) {
|
|
191
|
-
return Math.max(0, selected - 1);
|
|
192
|
-
}
|
|
193
|
-
if (matchesKey(data, Key.down)) {
|
|
194
|
-
return Math.min(Math.max(0, count - 1), selected + 1);
|
|
195
|
-
}
|
|
196
|
-
if (matchesKey(data, Key.enter)) {
|
|
197
|
-
done(selected);
|
|
198
|
-
}
|
|
199
|
-
return selected;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function getBrowserWindow(selected: number, total: number, pageSize: number): { start: number; end: number } {
|
|
357
|
+
total: number,
|
|
358
|
+
pageSize: number,
|
|
359
|
+
): { start: number; end: number } {
|
|
203
360
|
const halfWindow = Math.floor(pageSize / 2);
|
|
204
361
|
const maxStart = Math.max(0, total - pageSize);
|
|
205
362
|
const start = Math.max(0, Math.min(selected - halfWindow, maxStart));
|
|
@@ -230,7 +387,11 @@ function renderListBrowser<T>(
|
|
|
230
387
|
];
|
|
231
388
|
|
|
232
389
|
const pageSize = Math.min(config.pageSize, config.items.length);
|
|
233
|
-
const { start, end } = getBrowserWindow(
|
|
390
|
+
const { start, end } = getBrowserWindow(
|
|
391
|
+
config.selected,
|
|
392
|
+
config.items.length,
|
|
393
|
+
pageSize,
|
|
394
|
+
);
|
|
234
395
|
const visible = config.items.slice(start, end);
|
|
235
396
|
|
|
236
397
|
if (visible.length === 0) {
|
|
@@ -243,50 +404,61 @@ function renderListBrowser<T>(
|
|
|
243
404
|
}
|
|
244
405
|
|
|
245
406
|
if (start > 0) {
|
|
246
|
-
lines.push(
|
|
407
|
+
lines.push(
|
|
408
|
+
truncateToWidth(theme.fg("dim", `... ${start} more above`), width),
|
|
409
|
+
);
|
|
247
410
|
}
|
|
248
411
|
for (const [offset, item] of visible.entries()) {
|
|
249
412
|
const index = start + offset;
|
|
250
|
-
lines.push(
|
|
413
|
+
lines.push(
|
|
414
|
+
truncateToWidth(
|
|
415
|
+
config.renderItem(item, index, index === config.selected),
|
|
416
|
+
width,
|
|
417
|
+
),
|
|
418
|
+
);
|
|
251
419
|
}
|
|
252
420
|
if (end < config.items.length) {
|
|
253
|
-
lines.push(
|
|
421
|
+
lines.push(
|
|
422
|
+
truncateToWidth(
|
|
423
|
+
theme.fg("dim", `... ${config.items.length - end} more below`),
|
|
424
|
+
width,
|
|
425
|
+
),
|
|
426
|
+
);
|
|
254
427
|
}
|
|
255
428
|
|
|
256
429
|
lines.push("", truncateToWidth(theme.fg("dim", config.footer), width));
|
|
257
430
|
return lines;
|
|
258
431
|
}
|
|
259
432
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
export class DuplicationBrowser {
|
|
263
|
-
private selected = 0;
|
|
264
|
-
|
|
433
|
+
export class DuplicationBrowser extends SearchableListBrowser<FileDuplication> {
|
|
265
434
|
constructor(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
handleInput(data: string): void {
|
|
274
|
-
this.selected = handleListBrowserInput(data, this.selected, this.files.length, this.done);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
render(width: number): string[] {
|
|
278
|
-
return renderListBrowser(this.theme, width, {
|
|
435
|
+
files: FileDuplication[],
|
|
436
|
+
theme: Theme,
|
|
437
|
+
done: (result: number | null) => void,
|
|
438
|
+
scopeLabel?: string,
|
|
439
|
+
) {
|
|
440
|
+
const scopeSuffix = scopeLabel ? ` • ${scopeLabel}` : "";
|
|
441
|
+
super(files, theme, done, {
|
|
279
442
|
title: "SonarQube Duplications",
|
|
280
|
-
subtitle:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
emptyMessage: "No duplicate code found.",
|
|
443
|
+
subtitle: (_query, totalCount, filteredCount) =>
|
|
444
|
+
`${totalCount} file(s) with duplicate code${scopeSuffix} • ${filteredCount} match(es)`,
|
|
445
|
+
searchHint: "Search duplications by file path, duplicated lines, blocks, or density",
|
|
446
|
+
emptyMessage: "No matching duplicated files found.",
|
|
285
447
|
footer: "Up/Down to move, Enter for details, Esc to close",
|
|
448
|
+
pageSize: 15,
|
|
449
|
+
searchText: (file) =>
|
|
450
|
+
[
|
|
451
|
+
file.filePath,
|
|
452
|
+
`dup%=${file.duplicatedLinesDensity.toFixed(1)}`,
|
|
453
|
+
`blocks=${file.duplicatedBlocks}`,
|
|
454
|
+
`lines=${file.duplicatedLines}`,
|
|
455
|
+
].join(" "),
|
|
286
456
|
renderItem: (file, index, isSelected) => {
|
|
287
|
-
const marker = isSelected
|
|
288
|
-
|
|
289
|
-
|
|
457
|
+
const marker = isSelected
|
|
458
|
+
? theme.fg("accent", ">")
|
|
459
|
+
: theme.fg("dim", " ");
|
|
460
|
+
const detail = `dup%=${file.duplicatedLinesDensity.toFixed(1)} blocks=${file.duplicatedBlocks} lines=${file.duplicatedLines}`;
|
|
461
|
+
return `${marker} ${String(index + 1).padStart(2, " ")}. ${theme.fg("accent", file.filePath)} ${theme.fg("dim", detail)}`;
|
|
290
462
|
},
|
|
291
463
|
});
|
|
292
464
|
}
|
|
@@ -295,9 +467,13 @@ export class DuplicationBrowser {
|
|
|
295
467
|
export async function showDuplicationBrowser(
|
|
296
468
|
ctx: ExtensionCommandContext,
|
|
297
469
|
files: FileDuplication[],
|
|
470
|
+
scopeLabel?: string,
|
|
298
471
|
): Promise<number | null> {
|
|
299
472
|
if (ctx.mode !== "tui" || files.length === 0) return null;
|
|
300
|
-
return await ctx.ui.custom<number | null>(
|
|
473
|
+
return await ctx.ui.custom<number | null>(
|
|
474
|
+
(_tui, theme, _kb, done) =>
|
|
475
|
+
new DuplicationBrowser(files, theme, done, scopeLabel),
|
|
476
|
+
);
|
|
301
477
|
}
|
|
302
478
|
|
|
303
479
|
// ── Issue browser / preview helpers (used by index.ts) ──────────────────────
|
|
@@ -308,7 +484,9 @@ export async function showIssueBrowser(
|
|
|
308
484
|
): Promise<number | null> {
|
|
309
485
|
if (ctx.mode !== "tui") return null;
|
|
310
486
|
if (state.issues.length === 0) return null;
|
|
311
|
-
return await ctx.ui.custom<number | null>(
|
|
487
|
+
return await ctx.ui.custom<number | null>(
|
|
488
|
+
(_tui, theme, _kb, done) => new IssueBrowser(state, theme, done),
|
|
489
|
+
);
|
|
312
490
|
}
|
|
313
491
|
|
|
314
492
|
export async function openIssuePreview(
|
|
@@ -340,9 +518,17 @@ export async function loadProjectIssuesFromApi(
|
|
|
340
518
|
config.projectKey,
|
|
341
519
|
ctx.signal,
|
|
342
520
|
normalizedFilters,
|
|
521
|
+
config.baseDir,
|
|
522
|
+
);
|
|
523
|
+
const cleanCodeMode = await fetchCleanCodeMode(
|
|
524
|
+
config.serverUrl,
|
|
525
|
+
config.token,
|
|
526
|
+
ctx.signal,
|
|
343
527
|
);
|
|
344
|
-
|
|
345
|
-
|
|
528
|
+
return createAnalysisState(config, issues, {
|
|
529
|
+
filters: normalizedFilters,
|
|
530
|
+
cleanCodeMode,
|
|
531
|
+
});
|
|
346
532
|
}
|
|
347
533
|
|
|
348
534
|
export async function resolveTargetState(
|