@jonathan0823/pi-sonarqube 0.5.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 +80 -25
- 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 +226 -18
- package/dist/commands.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -35
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- 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 +130 -70
- package/dist/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +127 -30
- package/src/commands.ts +306 -20
- package/src/index.ts +87 -56
- package/src/types.ts +2 -0
- package/src/ui.ts +175 -84
package/src/index.ts
CHANGED
|
@@ -116,6 +116,12 @@ const SonarToolParams = Type.Object({
|
|
|
116
116
|
Type.String({ description: "Impact severity to fetch (e.g. HIGH)" }),
|
|
117
117
|
),
|
|
118
118
|
),
|
|
119
|
+
pathScope: Type.Optional(
|
|
120
|
+
Type.String({
|
|
121
|
+
description:
|
|
122
|
+
"Scope issues to a specific file or directory path (e.g. src/api.ts or src/). Use this instead of the `path` parameter for file-level filtering.",
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
119
125
|
});
|
|
120
126
|
|
|
121
127
|
// ── State restoration ───────────────────────────────────────────────────────
|
|
@@ -150,6 +156,7 @@ async function analyzeProject(
|
|
|
150
156
|
filters?: SonarIssueFetchOptions,
|
|
151
157
|
): Promise<SonarAnalysisState> {
|
|
152
158
|
const config = await resolveConfig(ctx, inputPath);
|
|
159
|
+
_cachedConfig = { serverUrl: config.serverUrl, projectKey: config.projectKey, token: config.token };
|
|
153
160
|
const baseDirStat = await stat(config.baseDir).catch(() => undefined);
|
|
154
161
|
if (!baseDirStat?.isDirectory()) {
|
|
155
162
|
throw new Error(`Project directory not found: ${config.baseDir}`);
|
|
@@ -183,6 +190,7 @@ async function analyzeProject(
|
|
|
183
190
|
config.projectKey,
|
|
184
191
|
ctx.signal,
|
|
185
192
|
normalizedFilters,
|
|
193
|
+
config.baseDir,
|
|
186
194
|
);
|
|
187
195
|
|
|
188
196
|
analysisUi.setPhase("Fetching metrics...");
|
|
@@ -385,6 +393,21 @@ function renderIssueListResult(
|
|
|
385
393
|
|
|
386
394
|
// ── Extension entrypoint ────────────────────────────────────────────────────
|
|
387
395
|
|
|
396
|
+
// ponytail: lazy config cache so rule autocomplete can fetch from the server
|
|
397
|
+
let _cachedConfig: { serverUrl: string; projectKey: string; token?: string } | undefined;
|
|
398
|
+
|
|
399
|
+
async function cachedServerConfig(cwd: string): Promise<typeof _cachedConfig> {
|
|
400
|
+
if (!_cachedConfig) {
|
|
401
|
+
try {
|
|
402
|
+
const cfg = await resolveConfig({ cwd });
|
|
403
|
+
_cachedConfig = { serverUrl: cfg.serverUrl, projectKey: cfg.projectKey, token: cfg.token };
|
|
404
|
+
} catch {
|
|
405
|
+
// fall through
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return _cachedConfig;
|
|
409
|
+
}
|
|
410
|
+
|
|
388
411
|
export default function sonarqube(pi: ExtensionAPI) {
|
|
389
412
|
const statesByBaseDir = new Map<string, SonarAnalysisState>();
|
|
390
413
|
let latestState: SonarAnalysisState | undefined;
|
|
@@ -418,12 +441,17 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
418
441
|
const argumentText = match[1] ?? "";
|
|
419
442
|
const { current: currentToken } =
|
|
420
443
|
splitSonarArgumentContext(argumentText);
|
|
444
|
+
const cfg = await cachedServerConfig(ctx.cwd);
|
|
421
445
|
const extraItems =
|
|
422
|
-
sonarArgumentCompletions(
|
|
446
|
+
(await sonarArgumentCompletions(
|
|
423
447
|
argumentText,
|
|
424
448
|
latestState?.issues,
|
|
425
449
|
latestState?.cleanCodeMode,
|
|
426
|
-
|
|
450
|
+
undefined,
|
|
451
|
+
cfg?.serverUrl,
|
|
452
|
+
cfg?.token,
|
|
453
|
+
cfg?.projectKey,
|
|
454
|
+
)) ?? [];
|
|
427
455
|
const currentSuggestions = await current.getSuggestions(
|
|
428
456
|
lines,
|
|
429
457
|
cursorLine,
|
|
@@ -473,6 +501,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
473
501
|
promptGuidelines: [
|
|
474
502
|
"Use sonarqube when the user asks to run a local SonarQube scan, inspect issues, or open an issue's source location.",
|
|
475
503
|
"Use the optional severity, status, type, rule, quality, and impact severity filters to fetch only the most relevant issues.",
|
|
504
|
+
"To scope issues to a specific file or directory, use the `pathScope` parameter (e.g. pathScope: 'src/api.ts') — do not use the `path` parameter for file filtering.",
|
|
476
505
|
],
|
|
477
506
|
parameters: SonarToolParams,
|
|
478
507
|
|
|
@@ -485,6 +514,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
485
514
|
rules: params.rules,
|
|
486
515
|
softwareQualities: params.softwareQualities,
|
|
487
516
|
impactSeverities: params.impactSeverities,
|
|
517
|
+
pathScope: params.pathScope,
|
|
488
518
|
});
|
|
489
519
|
assertFiltersNotAmbiguous(filters);
|
|
490
520
|
|
|
@@ -615,12 +645,18 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
615
645
|
pi.registerCommand("sonarqube", {
|
|
616
646
|
description:
|
|
617
647
|
"Run a local SonarQube analysis, browse the latest issues, or init project config",
|
|
618
|
-
getArgumentCompletions: (argumentPrefix) =>
|
|
619
|
-
|
|
648
|
+
getArgumentCompletions: async (argumentPrefix) => {
|
|
649
|
+
const cfg = await cachedServerConfig(process.cwd());
|
|
650
|
+
return sonarArgumentCompletions(
|
|
620
651
|
argumentPrefix,
|
|
621
652
|
latestState?.issues,
|
|
622
653
|
latestState?.cleanCodeMode,
|
|
623
|
-
|
|
654
|
+
process.cwd(),
|
|
655
|
+
cfg?.serverUrl,
|
|
656
|
+
cfg?.token,
|
|
657
|
+
cfg?.projectKey,
|
|
658
|
+
);
|
|
659
|
+
},
|
|
624
660
|
handler: async (args, ctx) => {
|
|
625
661
|
try {
|
|
626
662
|
const parsed = parseCommandArgs(args);
|
|
@@ -653,6 +689,7 @@ export default function sonarqube(pi: ExtensionAPI) {
|
|
|
653
689
|
ctx,
|
|
654
690
|
parsed.targetInput,
|
|
655
691
|
parsed.issueIndex,
|
|
692
|
+
parsed.filters,
|
|
656
693
|
);
|
|
657
694
|
return;
|
|
658
695
|
default:
|
|
@@ -693,38 +730,52 @@ async function commandAnalyze(
|
|
|
693
730
|
}
|
|
694
731
|
}
|
|
695
732
|
|
|
696
|
-
async function
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
): Promise<
|
|
700
|
-
|
|
733
|
+
async function loadMetricsData(
|
|
734
|
+
config: SonarProjectConfig,
|
|
735
|
+
signal?: AbortSignal,
|
|
736
|
+
): Promise<{
|
|
737
|
+
cleanCodeMode: Awaited<ReturnType<typeof fetchCleanCodeMode>>;
|
|
738
|
+
measures: Awaited<ReturnType<typeof fetchDuplicationMeasures>>;
|
|
739
|
+
issueCounts: Awaited<ReturnType<typeof fetchIssueSeverityCounts>>;
|
|
740
|
+
qualityCounts: Awaited<ReturnType<typeof fetchIssueQualityCounts>>;
|
|
741
|
+
} | null> {
|
|
701
742
|
const cleanCodeMode = await fetchCleanCodeMode(
|
|
702
743
|
config.serverUrl,
|
|
703
744
|
config.token,
|
|
704
|
-
|
|
745
|
+
signal,
|
|
705
746
|
);
|
|
706
747
|
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
707
748
|
fetchDuplicationMeasures(
|
|
708
749
|
config.serverUrl,
|
|
709
750
|
config.token,
|
|
710
751
|
config.projectKey,
|
|
711
|
-
|
|
752
|
+
signal,
|
|
712
753
|
),
|
|
713
754
|
fetchIssueSeverityCounts(
|
|
714
755
|
config.serverUrl,
|
|
715
756
|
config.token,
|
|
716
757
|
config.projectKey,
|
|
717
|
-
|
|
758
|
+
signal,
|
|
718
759
|
cleanCodeMode,
|
|
719
760
|
),
|
|
720
761
|
fetchIssueQualityCounts(
|
|
721
762
|
config.serverUrl,
|
|
722
763
|
config.token,
|
|
723
764
|
config.projectKey,
|
|
724
|
-
|
|
765
|
+
signal,
|
|
725
766
|
),
|
|
726
767
|
]);
|
|
727
|
-
if (!measures && !issueCounts && !qualityCounts)
|
|
768
|
+
if (!measures && !issueCounts && !qualityCounts) return null;
|
|
769
|
+
return { cleanCodeMode, measures, issueCounts, qualityCounts };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async function commandMetrics(
|
|
773
|
+
ctx: ExtensionCommandContext,
|
|
774
|
+
targetInput?: string,
|
|
775
|
+
): Promise<void> {
|
|
776
|
+
const config = await resolveConfig(ctx, targetInput);
|
|
777
|
+
const metrics = await loadMetricsData(config, ctx.signal);
|
|
778
|
+
if (!metrics) {
|
|
728
779
|
if (ctx.hasUI) {
|
|
729
780
|
ctx.ui.notify(
|
|
730
781
|
`Project "${config.projectKey}" has not been analyzed yet. Run /sonarqube analyze first.`,
|
|
@@ -735,10 +786,10 @@ async function commandMetrics(
|
|
|
735
786
|
}
|
|
736
787
|
const text = formatMetricsOutput({
|
|
737
788
|
projectKey: config.projectKey,
|
|
738
|
-
measures,
|
|
739
|
-
issueCounts,
|
|
740
|
-
issueQualityCounts: qualityCounts,
|
|
741
|
-
cleanCodeMode,
|
|
789
|
+
measures: metrics.measures,
|
|
790
|
+
issueCounts: metrics.issueCounts,
|
|
791
|
+
issueQualityCounts: metrics.qualityCounts,
|
|
792
|
+
cleanCodeMode: metrics.cleanCodeMode,
|
|
742
793
|
});
|
|
743
794
|
if (ctx.hasUI) {
|
|
744
795
|
ctx.ui.notify(text, "info");
|
|
@@ -750,6 +801,7 @@ async function commandDuplications(
|
|
|
750
801
|
ctx: ExtensionCommandContext,
|
|
751
802
|
targetInput?: string,
|
|
752
803
|
fileIndex?: number,
|
|
804
|
+
filters?: SonarIssueFetchOptions,
|
|
753
805
|
): Promise<void> {
|
|
754
806
|
const config = await resolveConfig(ctx, targetInput);
|
|
755
807
|
let files: FileDuplication[];
|
|
@@ -759,6 +811,8 @@ async function commandDuplications(
|
|
|
759
811
|
config.token,
|
|
760
812
|
config.projectKey,
|
|
761
813
|
ctx.signal,
|
|
814
|
+
filters?.pathScope,
|
|
815
|
+
config.baseDir,
|
|
762
816
|
);
|
|
763
817
|
} catch (error) {
|
|
764
818
|
const msg = sonarErrorMessage(error);
|
|
@@ -782,7 +836,7 @@ async function commandDuplications(
|
|
|
782
836
|
await showDuplicationDrillDown(ctx, config, files, fileIndex);
|
|
783
837
|
return;
|
|
784
838
|
}
|
|
785
|
-
await showDuplicationListOrBrowser(ctx, config, files);
|
|
839
|
+
await showDuplicationListOrBrowser(ctx, config, files, filters);
|
|
786
840
|
}
|
|
787
841
|
|
|
788
842
|
async function showDuplicationDrillDown(
|
|
@@ -819,12 +873,14 @@ async function showDuplicationListOrBrowser(
|
|
|
819
873
|
ctx: ExtensionCommandContext,
|
|
820
874
|
config: SonarProjectConfig,
|
|
821
875
|
files: FileDuplication[],
|
|
876
|
+
filters?: SonarIssueFetchOptions,
|
|
822
877
|
): Promise<void> {
|
|
823
878
|
if (ctx.mode !== "tui") {
|
|
824
879
|
ctx.ui.notify(formatDuplicationsList(files), "info");
|
|
825
880
|
return;
|
|
826
881
|
}
|
|
827
|
-
const
|
|
882
|
+
const scopeLabel = filters?.pathScope ? issueFilterLabel(filters) : undefined;
|
|
883
|
+
const choice = await showDuplicationBrowser(ctx, files, scopeLabel);
|
|
828
884
|
if (choice == null) return;
|
|
829
885
|
const file = files[choice];
|
|
830
886
|
const groups = await fetchFileDuplicationBlocks(
|
|
@@ -1060,33 +1116,8 @@ async function toolMetrics(
|
|
|
1060
1116
|
details: { error: "Project not configured." },
|
|
1061
1117
|
};
|
|
1062
1118
|
}
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
config.token,
|
|
1066
|
-
ctx.signal,
|
|
1067
|
-
);
|
|
1068
|
-
const [measures, issueCounts, qualityCounts] = await Promise.all([
|
|
1069
|
-
fetchDuplicationMeasures(
|
|
1070
|
-
config.serverUrl,
|
|
1071
|
-
config.token,
|
|
1072
|
-
config.projectKey,
|
|
1073
|
-
ctx.signal,
|
|
1074
|
-
),
|
|
1075
|
-
fetchIssueSeverityCounts(
|
|
1076
|
-
config.serverUrl,
|
|
1077
|
-
config.token,
|
|
1078
|
-
config.projectKey,
|
|
1079
|
-
ctx.signal,
|
|
1080
|
-
cleanCodeMode,
|
|
1081
|
-
),
|
|
1082
|
-
fetchIssueQualityCounts(
|
|
1083
|
-
config.serverUrl,
|
|
1084
|
-
config.token,
|
|
1085
|
-
config.projectKey,
|
|
1086
|
-
ctx.signal,
|
|
1087
|
-
),
|
|
1088
|
-
]);
|
|
1089
|
-
if (!measures && !issueCounts && !qualityCounts) {
|
|
1119
|
+
const metrics = await loadMetricsData(config, ctx.signal);
|
|
1120
|
+
if (!metrics) {
|
|
1090
1121
|
return {
|
|
1091
1122
|
content: [
|
|
1092
1123
|
{
|
|
@@ -1101,19 +1132,19 @@ async function toolMetrics(
|
|
|
1101
1132
|
}
|
|
1102
1133
|
const text = formatMetricsOutput({
|
|
1103
1134
|
projectKey: config.projectKey,
|
|
1104
|
-
measures,
|
|
1105
|
-
issueCounts,
|
|
1106
|
-
issueQualityCounts: qualityCounts,
|
|
1107
|
-
cleanCodeMode,
|
|
1135
|
+
measures: metrics.measures,
|
|
1136
|
+
issueCounts: metrics.issueCounts,
|
|
1137
|
+
issueQualityCounts: metrics.qualityCounts,
|
|
1138
|
+
cleanCodeMode: metrics.cleanCodeMode,
|
|
1108
1139
|
});
|
|
1109
1140
|
return {
|
|
1110
1141
|
content: [{ type: "text", text }],
|
|
1111
1142
|
details: {
|
|
1112
1143
|
projectKey: config.projectKey,
|
|
1113
|
-
measures,
|
|
1114
|
-
issueCounts,
|
|
1115
|
-
issueQualityCounts: qualityCounts,
|
|
1116
|
-
cleanCodeMode,
|
|
1144
|
+
measures: metrics.measures,
|
|
1145
|
+
issueCounts: metrics.issueCounts,
|
|
1146
|
+
issueQualityCounts: metrics.qualityCounts,
|
|
1147
|
+
cleanCodeMode: metrics.cleanCodeMode,
|
|
1117
1148
|
},
|
|
1118
1149
|
};
|
|
1119
1150
|
}
|
package/src/types.ts
CHANGED
package/src/ui.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
ExtensionCommandContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
import { Input, Key, matchesKey, truncateToWidth, type Focusable } from "@earendil-works/pi-tui";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import type {
|
|
@@ -159,59 +159,177 @@ export function startAnalysisUi(
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// ──
|
|
162
|
+
// ── Searchable browser helpers ─────────────────────────────────────────────
|
|
163
163
|
|
|
164
|
-
|
|
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
|
+
}
|
|
178
|
+
|
|
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 {
|
|
165
200
|
private selected = 0;
|
|
201
|
+
private query = "";
|
|
202
|
+
private filteredItems: T[];
|
|
203
|
+
private _focused = false;
|
|
204
|
+
private readonly searchInput = new Input();
|
|
166
205
|
|
|
167
206
|
constructor(
|
|
168
|
-
private readonly
|
|
207
|
+
private readonly items: readonly T[],
|
|
169
208
|
private readonly theme: Theme,
|
|
170
209
|
private readonly done: (result: number | null) => void,
|
|
171
|
-
|
|
210
|
+
private readonly config: SearchableBrowserConfig<T>,
|
|
211
|
+
) {
|
|
212
|
+
this.filteredItems = [...items];
|
|
213
|
+
}
|
|
214
|
+
|
|
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
|
+
}
|
|
172
223
|
|
|
173
|
-
invalidate(): void {
|
|
224
|
+
invalidate(): void {
|
|
225
|
+
this.searchInput.invalidate();
|
|
226
|
+
}
|
|
174
227
|
|
|
175
228
|
handleInput(data: string): void {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
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();
|
|
182
254
|
}
|
|
183
255
|
|
|
184
256
|
render(width: number): string[] {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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, {
|
|
189
300
|
title: "SonarQube Issues",
|
|
190
|
-
subtitle:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
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.",
|
|
195
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(" "),
|
|
196
316
|
renderItem: (issue, issueIndex, isSelected) => {
|
|
197
317
|
const marker = isSelected
|
|
198
|
-
?
|
|
199
|
-
:
|
|
200
|
-
const location = issue.line
|
|
201
|
-
? `${issue.filePath}:${issue.line}`
|
|
202
|
-
: issue.filePath;
|
|
318
|
+
? theme.fg("accent", ">")
|
|
319
|
+
: theme.fg("dim", " ");
|
|
320
|
+
const location = issue.line ? `${issue.filePath}:${issue.line}` : issue.filePath;
|
|
203
321
|
const rule = issue.ruleName
|
|
204
322
|
? `${issue.rule} (${issue.ruleName})`
|
|
205
323
|
: issue.rule;
|
|
206
|
-
const severity = severityColor(
|
|
324
|
+
const severity = severityColor(theme, issue.severity);
|
|
207
325
|
return [
|
|
208
326
|
marker,
|
|
209
327
|
String(issueIndex + 1).padStart(2, " "),
|
|
210
328
|
".",
|
|
211
329
|
severity,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
330
|
+
theme.fg("accent", location),
|
|
331
|
+
theme.fg("muted", rule),
|
|
332
|
+
theme.fg("text", `— ${issue.message}`),
|
|
215
333
|
].join(" ");
|
|
216
334
|
},
|
|
217
335
|
});
|
|
@@ -234,28 +352,6 @@ function severityColor(theme: Theme, severity: string): string {
|
|
|
234
352
|
}
|
|
235
353
|
}
|
|
236
354
|
|
|
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
355
|
function getBrowserWindow(
|
|
260
356
|
selected: number,
|
|
261
357
|
total: number,
|
|
@@ -334,43 +430,35 @@ function renderListBrowser<T>(
|
|
|
334
430
|
return lines;
|
|
335
431
|
}
|
|
336
432
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
export class DuplicationBrowser {
|
|
340
|
-
private selected = 0;
|
|
341
|
-
|
|
433
|
+
export class DuplicationBrowser extends SearchableListBrowser<FileDuplication> {
|
|
342
434
|
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, {
|
|
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, {
|
|
361
442
|
title: "SonarQube Duplications",
|
|
362
|
-
subtitle:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
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.",
|
|
367
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(" "),
|
|
368
456
|
renderItem: (file, index, isSelected) => {
|
|
369
457
|
const marker = isSelected
|
|
370
|
-
?
|
|
371
|
-
:
|
|
458
|
+
? theme.fg("accent", ">")
|
|
459
|
+
: theme.fg("dim", " ");
|
|
372
460
|
const detail = `dup%=${file.duplicatedLinesDensity.toFixed(1)} blocks=${file.duplicatedBlocks} lines=${file.duplicatedLines}`;
|
|
373
|
-
return `${marker} ${String(index + 1).padStart(2, " ")}. ${
|
|
461
|
+
return `${marker} ${String(index + 1).padStart(2, " ")}. ${theme.fg("accent", file.filePath)} ${theme.fg("dim", detail)}`;
|
|
374
462
|
},
|
|
375
463
|
});
|
|
376
464
|
}
|
|
@@ -379,10 +467,12 @@ export class DuplicationBrowser {
|
|
|
379
467
|
export async function showDuplicationBrowser(
|
|
380
468
|
ctx: ExtensionCommandContext,
|
|
381
469
|
files: FileDuplication[],
|
|
470
|
+
scopeLabel?: string,
|
|
382
471
|
): Promise<number | null> {
|
|
383
472
|
if (ctx.mode !== "tui" || files.length === 0) return null;
|
|
384
473
|
return await ctx.ui.custom<number | null>(
|
|
385
|
-
(_tui, theme, _kb, done) =>
|
|
474
|
+
(_tui, theme, _kb, done) =>
|
|
475
|
+
new DuplicationBrowser(files, theme, done, scopeLabel),
|
|
386
476
|
);
|
|
387
477
|
}
|
|
388
478
|
|
|
@@ -428,6 +518,7 @@ export async function loadProjectIssuesFromApi(
|
|
|
428
518
|
config.projectKey,
|
|
429
519
|
ctx.signal,
|
|
430
520
|
normalizedFilters,
|
|
521
|
+
config.baseDir,
|
|
431
522
|
);
|
|
432
523
|
const cleanCodeMode = await fetchCleanCodeMode(
|
|
433
524
|
config.serverUrl,
|