@jonathan0823/pi-sonarqube 0.5.0 → 0.7.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 +15 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +80 -25
- package/dist/api.js.map +1 -1
- package/dist/commands.d.ts +4 -2
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +260 -20
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -37
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui.d.ts +42 -13
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +154 -66
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +130 -33
- package/src/commands.ts +368 -27
- package/src/index.ts +117 -56
- package/src/types.ts +5 -1
- package/src/ui.ts +224 -80
package/src/ui.ts
CHANGED
|
@@ -3,7 +3,13 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
ExtensionCommandContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Input,
|
|
8
|
+
Key,
|
|
9
|
+
matchesKey,
|
|
10
|
+
truncateToWidth,
|
|
11
|
+
type Focusable,
|
|
12
|
+
} from "@earendil-works/pi-tui";
|
|
7
13
|
import { readFile } from "node:fs/promises";
|
|
8
14
|
import { resolve } from "node:path";
|
|
9
15
|
import type {
|
|
@@ -159,59 +165,181 @@ export function startAnalysisUi(
|
|
|
159
165
|
};
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
// ──
|
|
168
|
+
// ── Searchable browser helpers ─────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function normalizeSearchQuery(query: string): string[] {
|
|
171
|
+
return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function matchesSearchQuery(haystack: string, query: string): boolean {
|
|
175
|
+
const tokens = normalizeSearchQuery(query);
|
|
176
|
+
if (tokens.length === 0) return true;
|
|
177
|
+
const text = haystack.toLowerCase();
|
|
178
|
+
return tokens.every((token) => text.includes(token));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function filterSearchItems<T>(
|
|
182
|
+
items: readonly T[],
|
|
183
|
+
query: string,
|
|
184
|
+
searchText: (item: T) => string,
|
|
185
|
+
): T[] {
|
|
186
|
+
if (!query.trim()) return [...items];
|
|
187
|
+
return items.filter((item) => matchesSearchQuery(searchText(item), query));
|
|
188
|
+
}
|
|
163
189
|
|
|
164
|
-
|
|
190
|
+
interface SearchableBrowserConfig<T> {
|
|
191
|
+
title: string;
|
|
192
|
+
subtitle: (
|
|
193
|
+
query: string,
|
|
194
|
+
totalCount: number,
|
|
195
|
+
filteredCount: number,
|
|
196
|
+
) => string;
|
|
197
|
+
searchHint: string;
|
|
198
|
+
emptyMessage: string;
|
|
199
|
+
footer: string;
|
|
200
|
+
pageSize: number;
|
|
201
|
+
searchText: (item: T) => string;
|
|
202
|
+
renderItem: (item: T, index: number, isSelected: boolean) => string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class SearchableListBrowser<T> implements Focusable {
|
|
165
206
|
private selected = 0;
|
|
207
|
+
private query = "";
|
|
208
|
+
private filteredItems: T[];
|
|
209
|
+
private _focused = false;
|
|
210
|
+
private readonly searchInput = new Input();
|
|
166
211
|
|
|
167
212
|
constructor(
|
|
168
|
-
private readonly
|
|
213
|
+
private readonly items: readonly T[],
|
|
169
214
|
private readonly theme: Theme,
|
|
170
215
|
private readonly done: (result: number | null) => void,
|
|
171
|
-
|
|
216
|
+
private readonly config: SearchableBrowserConfig<T>,
|
|
217
|
+
) {
|
|
218
|
+
this.filteredItems = [...items];
|
|
219
|
+
}
|
|
172
220
|
|
|
173
|
-
|
|
221
|
+
get focused(): boolean {
|
|
222
|
+
return this._focused;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
set focused(value: boolean) {
|
|
226
|
+
this._focused = value;
|
|
227
|
+
this.searchInput.focused = value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
invalidate(): void {
|
|
231
|
+
this.searchInput.invalidate();
|
|
232
|
+
}
|
|
174
233
|
|
|
175
234
|
handleInput(data: string): void {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
235
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
236
|
+
this.done(null);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (matchesKey(data, Key.up)) {
|
|
241
|
+
this.selected = Math.max(0, this.selected - 1);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (matchesKey(data, Key.down)) {
|
|
246
|
+
this.selected = Math.min(
|
|
247
|
+
Math.max(0, this.filteredItems.length - 1),
|
|
248
|
+
this.selected + 1,
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (matchesKey(data, Key.enter)) {
|
|
254
|
+
if (this.filteredItems.length > 0) this.done(this.selected);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.searchInput.handleInput(data);
|
|
259
|
+
this.syncFilter();
|
|
182
260
|
}
|
|
183
261
|
|
|
184
262
|
render(width: number): string[] {
|
|
185
|
-
const
|
|
186
|
-
|
|
263
|
+
const subtitle = this.config.subtitle(
|
|
264
|
+
this.query,
|
|
265
|
+
this.items.length,
|
|
266
|
+
this.filteredItems.length,
|
|
267
|
+
);
|
|
268
|
+
return [
|
|
269
|
+
truncateToWidth(this.theme.fg("dim", this.config.searchHint), width),
|
|
270
|
+
...this.searchInput.render(width),
|
|
271
|
+
"",
|
|
272
|
+
...renderListBrowser(this.theme, width, {
|
|
273
|
+
title: this.config.title,
|
|
274
|
+
subtitle,
|
|
275
|
+
items: this.filteredItems,
|
|
276
|
+
selected: this.selected,
|
|
277
|
+
pageSize: this.config.pageSize,
|
|
278
|
+
emptyMessage: this.config.emptyMessage,
|
|
279
|
+
footer: this.config.footer,
|
|
280
|
+
renderItem: this.config.renderItem,
|
|
281
|
+
}),
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private syncFilter(): void {
|
|
286
|
+
const nextQuery = this.searchInput.getValue();
|
|
287
|
+
if (nextQuery === this.query) return;
|
|
288
|
+
this.query = nextQuery;
|
|
289
|
+
this.filteredItems = filterSearchItems(
|
|
290
|
+
this.items,
|
|
291
|
+
nextQuery,
|
|
292
|
+
this.config.searchText,
|
|
293
|
+
);
|
|
294
|
+
this.selected = 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export class IssueBrowser extends SearchableListBrowser<SonarIssue> {
|
|
299
|
+
constructor(
|
|
300
|
+
state: SonarAnalysisState,
|
|
301
|
+
theme: Theme,
|
|
302
|
+
done: (result: number | null) => void,
|
|
303
|
+
) {
|
|
304
|
+
const filterSuffix = state.filters
|
|
305
|
+
? ` • ${issueFilterLabel(state.filters)}`
|
|
187
306
|
: "";
|
|
188
|
-
|
|
307
|
+
super(state.issues, theme, done, {
|
|
189
308
|
title: "SonarQube Issues",
|
|
190
|
-
subtitle:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
emptyMessage: "No open issues found.",
|
|
309
|
+
subtitle: (_query, totalCount, filteredCount) =>
|
|
310
|
+
`${state.projectKey} • ${totalCount} issue(s) • ${filteredCount} match(es)${filterSuffix}`,
|
|
311
|
+
searchHint: "Search issues by file, rule, severity, status, or message",
|
|
312
|
+
emptyMessage: "No matching issues found.",
|
|
195
313
|
footer: "Up/Down to move, Enter to preview, Esc to close",
|
|
314
|
+
pageSize: 20,
|
|
315
|
+
searchText: (issue) =>
|
|
316
|
+
[
|
|
317
|
+
issue.filePath,
|
|
318
|
+
issue.rule,
|
|
319
|
+
issue.ruleName ?? "",
|
|
320
|
+
issue.severity,
|
|
321
|
+
issue.status ?? "",
|
|
322
|
+
issue.message,
|
|
323
|
+
].join(" "),
|
|
196
324
|
renderItem: (issue, issueIndex, isSelected) => {
|
|
197
325
|
const marker = isSelected
|
|
198
|
-
?
|
|
199
|
-
:
|
|
326
|
+
? theme.fg("accent", ">")
|
|
327
|
+
: theme.fg("dim", " ");
|
|
200
328
|
const location = issue.line
|
|
201
329
|
? `${issue.filePath}:${issue.line}`
|
|
202
330
|
: issue.filePath;
|
|
203
331
|
const rule = issue.ruleName
|
|
204
332
|
? `${issue.rule} (${issue.ruleName})`
|
|
205
333
|
: issue.rule;
|
|
206
|
-
const severity = severityColor(
|
|
334
|
+
const severity = severityColor(theme, issue.severity);
|
|
207
335
|
return [
|
|
208
336
|
marker,
|
|
209
337
|
String(issueIndex + 1).padStart(2, " "),
|
|
210
338
|
".",
|
|
211
339
|
severity,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
340
|
+
theme.fg("accent", location),
|
|
341
|
+
theme.fg("muted", rule),
|
|
342
|
+
theme.fg("text", `— ${issue.message}`),
|
|
215
343
|
].join(" ");
|
|
216
344
|
},
|
|
217
345
|
});
|
|
@@ -234,28 +362,6 @@ function severityColor(theme: Theme, severity: string): string {
|
|
|
234
362
|
}
|
|
235
363
|
}
|
|
236
364
|
|
|
237
|
-
function handleListBrowserInput(
|
|
238
|
-
data: string,
|
|
239
|
-
selected: number,
|
|
240
|
-
count: number,
|
|
241
|
-
done: (result: number | null) => void,
|
|
242
|
-
): number {
|
|
243
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, "ctrl+c")) {
|
|
244
|
-
done(null);
|
|
245
|
-
return selected;
|
|
246
|
-
}
|
|
247
|
-
if (matchesKey(data, Key.up)) {
|
|
248
|
-
return Math.max(0, selected - 1);
|
|
249
|
-
}
|
|
250
|
-
if (matchesKey(data, Key.down)) {
|
|
251
|
-
return Math.min(Math.max(0, count - 1), selected + 1);
|
|
252
|
-
}
|
|
253
|
-
if (matchesKey(data, Key.enter)) {
|
|
254
|
-
done(selected);
|
|
255
|
-
}
|
|
256
|
-
return selected;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
365
|
function getBrowserWindow(
|
|
260
366
|
selected: number,
|
|
261
367
|
total: number,
|
|
@@ -334,43 +440,36 @@ function renderListBrowser<T>(
|
|
|
334
440
|
return lines;
|
|
335
441
|
}
|
|
336
442
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
export class DuplicationBrowser {
|
|
340
|
-
private selected = 0;
|
|
341
|
-
|
|
443
|
+
export class DuplicationBrowser extends SearchableListBrowser<FileDuplication> {
|
|
342
444
|
constructor(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
handleInput(data: string): void {
|
|
351
|
-
this.selected = handleListBrowserInput(
|
|
352
|
-
data,
|
|
353
|
-
this.selected,
|
|
354
|
-
this.files.length,
|
|
355
|
-
this.done,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
render(width: number): string[] {
|
|
360
|
-
return renderListBrowser(this.theme, width, {
|
|
445
|
+
files: FileDuplication[],
|
|
446
|
+
theme: Theme,
|
|
447
|
+
done: (result: number | null) => void,
|
|
448
|
+
scopeLabel?: string,
|
|
449
|
+
) {
|
|
450
|
+
const scopeSuffix = scopeLabel ? ` • ${scopeLabel}` : "";
|
|
451
|
+
super(files, theme, done, {
|
|
361
452
|
title: "SonarQube Duplications",
|
|
362
|
-
subtitle:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
emptyMessage: "No
|
|
453
|
+
subtitle: (_query, totalCount, filteredCount) =>
|
|
454
|
+
`${totalCount} file(s) with duplicate code${scopeSuffix} • ${filteredCount} match(es)`,
|
|
455
|
+
searchHint:
|
|
456
|
+
"Search duplications by file path, duplicated lines, blocks, or density",
|
|
457
|
+
emptyMessage: "No matching duplicated files found.",
|
|
367
458
|
footer: "Up/Down to move, Enter for details, Esc to close",
|
|
459
|
+
pageSize: 15,
|
|
460
|
+
searchText: (file) =>
|
|
461
|
+
[
|
|
462
|
+
file.filePath,
|
|
463
|
+
`dup%=${file.duplicatedLinesDensity.toFixed(1)}`,
|
|
464
|
+
`blocks=${file.duplicatedBlocks}`,
|
|
465
|
+
`lines=${file.duplicatedLines}`,
|
|
466
|
+
].join(" "),
|
|
368
467
|
renderItem: (file, index, isSelected) => {
|
|
369
468
|
const marker = isSelected
|
|
370
|
-
?
|
|
371
|
-
:
|
|
469
|
+
? theme.fg("accent", ">")
|
|
470
|
+
: theme.fg("dim", " ");
|
|
372
471
|
const detail = `dup%=${file.duplicatedLinesDensity.toFixed(1)} blocks=${file.duplicatedBlocks} lines=${file.duplicatedLines}`;
|
|
373
|
-
return `${marker} ${String(index + 1).padStart(2, " ")}. ${
|
|
472
|
+
return `${marker} ${String(index + 1).padStart(2, " ")}. ${theme.fg("accent", file.filePath)} ${theme.fg("dim", detail)}`;
|
|
374
473
|
},
|
|
375
474
|
});
|
|
376
475
|
}
|
|
@@ -379,11 +478,55 @@ export class DuplicationBrowser {
|
|
|
379
478
|
export async function showDuplicationBrowser(
|
|
380
479
|
ctx: ExtensionCommandContext,
|
|
381
480
|
files: FileDuplication[],
|
|
481
|
+
scopeLabel?: string,
|
|
382
482
|
): Promise<number | null> {
|
|
383
483
|
if (ctx.mode !== "tui" || files.length === 0) return null;
|
|
384
484
|
return await ctx.ui.custom<number | null>(
|
|
385
|
-
(_tui, theme, _kb, done) =>
|
|
485
|
+
(_tui, theme, _kb, done) =>
|
|
486
|
+
new DuplicationBrowser(files, theme, done, scopeLabel),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Workspace selector browser ────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
export class WorkspaceBrowser extends SearchableListBrowser<{
|
|
493
|
+
alias: string;
|
|
494
|
+
path: string;
|
|
495
|
+
}> {
|
|
496
|
+
constructor(
|
|
497
|
+
workspaces: { alias: string; path: string }[],
|
|
498
|
+
theme: Theme,
|
|
499
|
+
done: (result: number | null) => void,
|
|
500
|
+
) {
|
|
501
|
+
super(workspaces, theme, done, {
|
|
502
|
+
title: "SonarQube Workspaces",
|
|
503
|
+
subtitle: (_query, totalCount, filteredCount) =>
|
|
504
|
+
`${totalCount} workspace(s) • ${filteredCount} match(es)`,
|
|
505
|
+
searchHint: "Search workspaces by alias or path",
|
|
506
|
+
emptyMessage: "No matching workspaces found.",
|
|
507
|
+
footer: "Up/Down to move, Enter to select, Esc to cancel",
|
|
508
|
+
pageSize: 15,
|
|
509
|
+
searchText: (ws) => `${ws.alias} ${ws.path}`,
|
|
510
|
+
renderItem: (ws, index, isSelected) => {
|
|
511
|
+
const marker = isSelected
|
|
512
|
+
? theme.fg("accent", ">")
|
|
513
|
+
: theme.fg("dim", " ");
|
|
514
|
+
return `${marker} ${String(index + 1).padStart(2, " ")}. ${theme.fg("accent", ws.alias.padEnd(20))} ${theme.fg("dim", ws.path)}`;
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export async function showWorkspaceBrowser(
|
|
521
|
+
ctx: ExtensionContext,
|
|
522
|
+
workspaces: { alias: string; path: string }[],
|
|
523
|
+
): Promise<string | null> {
|
|
524
|
+
if (ctx.mode !== "tui" || workspaces.length === 0) return null;
|
|
525
|
+
const choice = await ctx.ui.custom<number | null>(
|
|
526
|
+
(_tui, theme, _kb, done) => new WorkspaceBrowser(workspaces, theme, done),
|
|
386
527
|
);
|
|
528
|
+
if (choice == null) return null;
|
|
529
|
+
return workspaces[choice].alias;
|
|
387
530
|
}
|
|
388
531
|
|
|
389
532
|
// ── Issue browser / preview helpers (used by index.ts) ──────────────────────
|
|
@@ -428,6 +571,7 @@ export async function loadProjectIssuesFromApi(
|
|
|
428
571
|
config.projectKey,
|
|
429
572
|
ctx.signal,
|
|
430
573
|
normalizedFilters,
|
|
574
|
+
config.baseDir,
|
|
431
575
|
);
|
|
432
576
|
const cleanCodeMode = await fetchCleanCodeMode(
|
|
433
577
|
config.serverUrl,
|