diff --git a/packages/amazonq/.changes/next-release/Feature-f0d4f2ca-72c5-4434-8cf3-fcd742bf33d7.json b/packages/amazonq/.changes/next-release/Feature-f0d4f2ca-72c5-4434-8cf3-fcd742bf33d7.json new file mode 100644 index 00000000000..bd6c8bf7549 --- /dev/null +++ b/packages/amazonq/.changes/next-release/Feature-f0d4f2ca-72c5-4434-8cf3-fcd742bf33d7.json @@ -0,0 +1,4 @@ +{ + "type": "Feature", + "description": "/review: Code issues can be grouped by file location or severity" +} diff --git a/packages/amazonq/package.json b/packages/amazonq/package.json index 1c8f68cf5b2..9aeda69ec65 100644 --- a/packages/amazonq/package.json +++ b/packages/amazonq/package.json @@ -365,10 +365,15 @@ "when": "view == aws.AmazonQChatView || view == aws.amazonq.AmazonCommonAuth", "group": "y_toolkitMeta@2" }, + { + "command": "aws.amazonq.codescan.showGroupingStrategy", + "when": "view == aws.amazonq.SecurityIssuesTree", + "group": "navigation@1" + }, { "command": "aws.amazonq.security.showFilters", "when": "view == aws.amazonq.SecurityIssuesTree", - "group": "navigation" + "group": "navigation@2" } ], "view/item/context": [ @@ -724,6 +729,12 @@ { "command": "aws.amazonq.security.showFilters", "title": "%AWS.command.amazonq.filterIssues%", + "icon": "$(filter)", + "enablement": "view == aws.amazonq.SecurityIssuesTree" + }, + { + "command": "aws.amazonq.codescan.showGroupingStrategy", + "title": "%AWS.command.amazonq.groupIssues%", "icon": "$(list-filter)", "enablement": "view == aws.amazonq.SecurityIssuesTree" }, diff --git a/packages/amazonq/test/unit/codewhisperer/models/model.test.ts b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts index ae7114a22c8..7b0888521f4 100644 --- a/packages/amazonq/test/unit/codewhisperer/models/model.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/models/model.test.ts @@ -4,7 +4,12 @@ */ import assert from 'assert' import sinon from 'sinon' -import { SecurityIssueFilters, SecurityTreeViewFilterState } from 'aws-core-vscode/codewhisperer' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + SecurityIssueFilters, + SecurityTreeViewFilterState, +} from 'aws-core-vscode/codewhisperer' import { globals } from 'aws-core-vscode/shared' describe('model', function () { @@ -70,4 +75,100 @@ describe('model', function () { assert.deepStrictEqual(hiddenSeverities, ['High', 'Low']) }) }) + + describe('CodeIssueGroupingStrategyState', function () { + let sandbox: sinon.SinonSandbox + let state: CodeIssueGroupingStrategyState + + beforeEach(function () { + sandbox = sinon.createSandbox() + state = CodeIssueGroupingStrategyState.instance + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('instance', function () { + it('should return the same instance when called multiple times', function () { + const instance1 = CodeIssueGroupingStrategyState.instance + const instance2 = CodeIssueGroupingStrategyState.instance + assert.strictEqual(instance1, instance2) + }) + }) + + describe('getState', function () { + it('should return fallback when no state is stored', function () { + const result = state.getState() + + assert.equal(result, CodeIssueGroupingStrategy.Severity) + }) + + it('should return stored state when valid', async function () { + const validStrategy = CodeIssueGroupingStrategy.FileLocation + await state.setState(validStrategy) + + const result = state.getState() + + assert.equal(result, validStrategy) + }) + + it('should return fallback when stored state is invalid', async function () { + const invalidStrategy = 'invalid' + await state.setState(invalidStrategy) + + const result = state.getState() + + assert.equal(result, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('setState', function () { + it('should update state and fire change event for valid strategy', async function () { + const validStrategy = CodeIssueGroupingStrategy.FileLocation + + // Create a spy to watch for event emissions + const eventSpy = sandbox.spy() + state.onDidChangeState(eventSpy) + + await state.setState(validStrategy) + + sinon.assert.calledWith(eventSpy, validStrategy) + }) + + it('should use fallback and fire change event for invalid strategy', async function () { + const invalidStrategy = 'invalid' + + // Create a spy to watch for event emissions + const eventSpy = sandbox.spy() + state.onDidChangeState(eventSpy) + + await state.setState(invalidStrategy) + + sinon.assert.calledWith(eventSpy, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('reset', function () { + it('should set state to fallback value', async function () { + const setStateStub = sandbox.stub(state, 'setState').resolves() + + await state.reset() + + sinon.assert.calledWith(setStateStub, CodeIssueGroupingStrategy.Severity) + }) + }) + + describe('onDidChangeState', function () { + it('should allow subscribing to state changes', async function () { + const listener = sandbox.spy() + const disposable = state.onDidChangeState(listener) + + await state.setState(CodeIssueGroupingStrategy.Severity) + + sinon.assert.calledWith(listener, CodeIssueGroupingStrategy.Severity) + disposable.dispose() + }) + }) + }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts index bd7c3aab8de..4d973735c9f 100644 --- a/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts +++ b/packages/amazonq/test/unit/codewhisperer/service/securityIssueTreeViewProvider.test.ts @@ -10,17 +10,24 @@ import { SecurityTreeViewFilterState, SecurityIssueProvider, SeverityItem, + CodeIssueGroupingStrategyState, + CodeIssueGroupingStrategy, } from 'aws-core-vscode/codewhisperer' import { createCodeScanIssue } from 'aws-core-vscode/test' import assert from 'assert' import sinon from 'sinon' +import path from 'path' describe('SecurityIssueTreeViewProvider', function () { - let securityIssueProvider: SecurityIssueProvider let securityIssueTreeViewProvider: SecurityIssueTreeViewProvider beforeEach(function () { - securityIssueProvider = SecurityIssueProvider.instance + SecurityIssueProvider.instance.issues = [ + { filePath: 'file/path/a', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/b', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/c', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + { filePath: 'file/path/d', issues: [createCodeScanIssue(), createCodeScanIssue()] }, + ] securityIssueTreeViewProvider = new SecurityIssueTreeViewProvider() }) @@ -44,13 +51,6 @@ describe('SecurityIssueTreeViewProvider', function () { describe('getChildren', function () { it('should return sorted list of severities if element is undefined', function () { - securityIssueProvider.issues = [ - { filePath: 'file/path/c', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/d', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/a', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - { filePath: 'file/path/b', issues: [createCodeScanIssue(), createCodeScanIssue()] }, - ] - const element = undefined const result = securityIssueTreeViewProvider.getChildren(element) as SeverityItem[] assert.strictEqual(result.length, 5) @@ -102,5 +102,55 @@ describe('SecurityIssueTreeViewProvider', function () { const result = securityIssueTreeViewProvider.getChildren(element) as IssueItem[] assert.strictEqual(result.length, 0) }) + + it('should return severity-grouped items when grouping strategy is Severity', function () { + sinon.stub(CodeIssueGroupingStrategyState.instance, 'getState').returns(CodeIssueGroupingStrategy.Severity) + + const severityItems = securityIssueTreeViewProvider.getChildren() as SeverityItem[] + for (const [index, [severity, expectedIssueCount]] of [ + ['Critical', 0], + ['High', 8], + ['Medium', 0], + ['Low', 0], + ['Info', 0], + ].entries()) { + const currentSeverityItem = severityItems[index] + assert.strictEqual(currentSeverityItem.label, severity) + assert.strictEqual(currentSeverityItem.issues.length, expectedIssueCount) + + const issueItems = securityIssueTreeViewProvider.getChildren(currentSeverityItem) as IssueItem[] + assert.ok(issueItems.every((item) => item.iconPath === undefined)) + assert.ok( + issueItems.every((item) => item.description?.toString().startsWith(path.basename(item.filePath))) + ) + } + }) + + it('should return file-grouped items when grouping strategy is FileLocation', function () { + sinon + .stub(CodeIssueGroupingStrategyState.instance, 'getState') + .returns(CodeIssueGroupingStrategy.FileLocation) + + const result = securityIssueTreeViewProvider.getChildren() as FileItem[] + for (const [index, [fileName, expectedIssueCount]] of [ + ['a', 2], + ['b', 2], + ['c', 2], + ['d', 2], + ].entries()) { + const currentFileItem = result[index] + assert.strictEqual(currentFileItem.label, fileName) + assert.strictEqual(currentFileItem.issues.length, expectedIssueCount) + assert.strictEqual(currentFileItem.description, 'file/path') + + const issueItems = securityIssueTreeViewProvider.getChildren(currentFileItem) as IssueItem[] + assert.ok( + issueItems.every((item) => + item.iconPath?.toString().includes(`${item.issue.severity.toLowerCase()}.svg`) + ) + ) + assert.ok(issueItems.every((item) => item.description?.toString().startsWith('[Ln '))) + } + }) }) }) diff --git a/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts b/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts new file mode 100644 index 00000000000..9c5e00cd6f7 --- /dev/null +++ b/packages/amazonq/test/unit/codewhisperer/ui/prompters.test.ts @@ -0,0 +1,45 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { createQuickPickPrompterTester, QuickPickPrompterTester } from 'aws-core-vscode/test' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + createCodeIssueGroupingStrategyPrompter, +} from 'aws-core-vscode/codewhisperer' +import sinon from 'sinon' +import assert from 'assert' +import vscode from 'vscode' + +const severity = { data: CodeIssueGroupingStrategy.Severity, label: 'Severity' } +const fileLocation = { data: CodeIssueGroupingStrategy.FileLocation, label: 'File Location' } + +describe('createCodeIssueGroupingStrategyPrompter', function () { + let tester: QuickPickPrompterTester + + beforeEach(function () { + tester = createQuickPickPrompterTester(createCodeIssueGroupingStrategyPrompter()) + }) + + afterEach(function () { + sinon.restore() + }) + + it('should list grouping strategies', async function () { + tester.assertItems([severity, fileLocation]) + tester.hide() + await tester.result() + }) + + it('should update state on selection', async function () { + const originalState = CodeIssueGroupingStrategyState.instance.getState() + assert.equal(originalState, CodeIssueGroupingStrategy.Severity) + + tester.selectItems(fileLocation) + tester.addCallback(() => vscode.commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem')) + + await tester.result() + assert.equal(CodeIssueGroupingStrategyState.instance.getState(), fileLocation.data) + }) +}) diff --git a/packages/core/package.nls.json b/packages/core/package.nls.json index 1a8cbd817a0..833ed7aad7f 100644 --- a/packages/core/package.nls.json +++ b/packages/core/package.nls.json @@ -133,6 +133,7 @@ "AWS.command.amazonq.acceptFix": "Accept Fix", "AWS.command.amazonq.regenerateFix": "Regenerate Fix", "AWS.command.amazonq.filterIssues": "Filter Issues", + "AWS.command.amazonq.groupIssues": "Group Issues", "AWS.command.deploySamApplication": "Deploy SAM Application", "AWS.command.aboutToolkit": "About", "AWS.command.downloadLambda": "Download...", @@ -309,6 +310,10 @@ "AWS.amazonq.scans.projectScanInProgress": "Workspace review is in progress...", "AWS.amazonq.scans.fileScanInProgress": "File review is in progress...", "AWS.amazonq.scans.noGitRepo": "Your workspace is not in a git repository. I'll review your project files for security issues, and your in-flight changes for code quality issues.", + "AWS.amazonq.scans.severity": "Severity", + "AWS.amazonq.scans.fileLocation": "File Location", + "AWS.amazonq.scans.groupIssues": "Group Issues", + "AWS.amazonq.scans.groupIssues.placeholder": "Select how to group code issues", "AWS.amazonq.featureDev.error.conversationIdNotFoundError": "Conversation id must exist before starting code generation", "AWS.amazonq.featureDev.error.contentLengthError": "The folder you selected is too large for me to use as context. Please choose a smaller folder to work on. For more information on quotas, see the Amazon Q Developer documentation.", "AWS.amazonq.featureDev.error.illegalStateTransition": "Illegal transition between states, restart the conversation", diff --git a/packages/core/src/codewhisperer/activation.ts b/packages/core/src/codewhisperer/activation.ts index 7e4c2452185..72516c06537 100644 --- a/packages/core/src/codewhisperer/activation.ts +++ b/packages/core/src/codewhisperer/activation.ts @@ -18,6 +18,7 @@ import { SecurityTreeViewFilterState, AggregatedCodeScanIssue, CodeScanIssue, + CodeIssueGroupingStrategyState, } from './models/model' import { invokeRecommendation } from './commands/invokeRecommendation' import { acceptSuggestion } from './commands/onInlineAcceptance' @@ -60,6 +61,7 @@ import { ignoreAllIssues, focusIssue, showExploreAgentsView, + showCodeIssueGroupingQuickPick, } from './commands/basicCommands' import { sleep } from '../shared/utilities/timeoutUtils' import { ReferenceLogViewProvider } from './service/referenceLogViewProvider' @@ -289,6 +291,8 @@ export async function activate(context: ExtContext): Promise { listCodeWhispererCommands.register(), // quick pick with security issues tree filters showSecurityIssueFilters.register(), + // quick pick code issue grouping strategy + showCodeIssueGroupingQuickPick.register(), // reset security issue filters clearFilters.register(), // handle security issues tree item clicked @@ -297,6 +301,10 @@ export async function activate(context: ExtContext): Promise { SecurityTreeViewFilterState.instance.onDidChangeState((e) => { SecurityIssueTreeViewProvider.instance.refresh() }), + // refresh treeview when grouping strategy changes + CodeIssueGroupingStrategyState.instance.onDidChangeState((e) => { + SecurityIssueTreeViewProvider.instance.refresh() + }), // show a no match state SecurityIssueTreeViewProvider.instance.onDidChangeTreeData((e) => { const noMatches = diff --git a/packages/core/src/codewhisperer/client/codewhisperer.ts b/packages/core/src/codewhisperer/client/codewhisperer.ts index 7a869a68372..47f6fa6e1b6 100644 --- a/packages/core/src/codewhisperer/client/codewhisperer.ts +++ b/packages/core/src/codewhisperer/client/codewhisperer.ts @@ -31,8 +31,8 @@ export interface CodeWhispererConfig { } export const defaultServiceConfig: CodeWhispererConfig = { - region: 'us-east-1', - endpoint: 'https://codewhisperer.us-east-1.amazonaws.com/', + region: 'us-west-2', + endpoint: 'https://rts.alpha-us-west-2.codewhisperer.ai.aws.dev/', } export function getCodewhispererConfig(): CodeWhispererConfig { diff --git a/packages/core/src/codewhisperer/commands/basicCommands.ts b/packages/core/src/codewhisperer/commands/basicCommands.ts index cbdf1b4e30b..f35bb859a0d 100644 --- a/packages/core/src/codewhisperer/commands/basicCommands.ts +++ b/packages/core/src/codewhisperer/commands/basicCommands.ts @@ -68,6 +68,7 @@ import { DefaultAmazonQAppInitContext } from '../../amazonq/apps/initContext' import path from 'path' import { UserWrittenCodeTracker } from '../tracker/userWrittenCodeTracker' import { parsePatch } from 'diff' +import { createCodeIssueGroupingStrategyPrompter } from '../ui/prompters' const MessageTimeOut = 5_000 @@ -887,6 +888,14 @@ export const showSecurityIssueFilters = Commands.declare({ id: 'aws.amazonq.secu } }) +export const showCodeIssueGroupingQuickPick = Commands.declare( + { id: 'aws.amazonq.codescan.showGroupingStrategy' }, + () => async () => { + const prompter = createCodeIssueGroupingStrategyPrompter() + await prompter.prompt() + } +) + export const focusIssue = Commands.declare( { id: 'aws.amazonq.security.focusIssue' }, () => async (issue: CodeScanIssue, filePath: string) => { diff --git a/packages/core/src/codewhisperer/index.ts b/packages/core/src/codewhisperer/index.ts index f2e15ee17ea..5484411a7ac 100644 --- a/packages/core/src/codewhisperer/index.ts +++ b/packages/core/src/codewhisperer/index.ts @@ -102,4 +102,5 @@ export * as CodeWhispererConstants from '../codewhisperer/models/constants' export { getSelectedCustomization, setSelectedCustomization, baseCustomization } from './util/customizationUtil' export { Container } from './service/serviceContainer' export * from './util/gitUtil' +export * from './ui/prompters' export { UserWrittenCodeTracker } from './tracker/userWrittenCodeTracker' diff --git a/packages/core/src/codewhisperer/models/model.ts b/packages/core/src/codewhisperer/models/model.ts index c9b3ca7c51a..5c5e945b14b 100644 --- a/packages/core/src/codewhisperer/models/model.ts +++ b/packages/core/src/codewhisperer/models/model.ts @@ -20,6 +20,7 @@ import { TransformationSteps } from '../client/codewhispereruserclient' import { Messenger } from '../../amazonqGumby/chat/controller/messenger/messenger' import { TestChatControllerEventEmitters } from '../../amazonqTest/chat/controller/controller' import { ScanChatControllerEventEmitters } from '../../amazonqScan/controller' +import { localize } from '../../shared/utilities/vsCodeUtils' // unavoidable global variables interface VsCodeState { @@ -564,6 +565,52 @@ export class SecurityTreeViewFilterState { } } +export enum CodeIssueGroupingStrategy { + Severity = 'Severity', + FileLocation = 'FileLocation', +} +const defaultCodeIssueGroupingStrategy = CodeIssueGroupingStrategy.Severity + +export const codeIssueGroupingStrategies = Object.values(CodeIssueGroupingStrategy) +export const codeIssueGroupingStrategyLabel: Record = { + [CodeIssueGroupingStrategy.Severity]: localize('AWS.amazonq.scans.severity', 'Severity'), + [CodeIssueGroupingStrategy.FileLocation]: localize('AWS.amazonq.scans.fileLocation', 'File Location'), +} + +export class CodeIssueGroupingStrategyState { + #fallback: CodeIssueGroupingStrategy + #onDidChangeState = new vscode.EventEmitter() + onDidChangeState = this.#onDidChangeState.event + + static #instance: CodeIssueGroupingStrategyState + static get instance() { + return (this.#instance ??= new this()) + } + + protected constructor(fallback: CodeIssueGroupingStrategy = defaultCodeIssueGroupingStrategy) { + this.#fallback = fallback + } + + public getState(): CodeIssueGroupingStrategy { + const state = globals.globalState.tryGet('aws.amazonq.codescan.groupingStrategy', String) + return this.isValidGroupingStrategy(state) ? state : this.#fallback + } + + public async setState(_state: unknown) { + const state = this.isValidGroupingStrategy(_state) ? _state : this.#fallback + await globals.globalState.update('aws.amazonq.codescan.groupingStrategy', state) + this.#onDidChangeState.fire(state) + } + + private isValidGroupingStrategy(strategy: unknown): strategy is CodeIssueGroupingStrategy { + return Object.values(CodeIssueGroupingStrategy).includes(strategy as CodeIssueGroupingStrategy) + } + + public reset() { + return this.setState(this.#fallback) + } +} + /** * Q - Transform */ diff --git a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts index e76a201be87..47490f2427f 100644 --- a/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts +++ b/packages/core/src/codewhisperer/service/securityIssueTreeViewProvider.ts @@ -4,7 +4,14 @@ */ import * as vscode from 'vscode' import path from 'path' -import { CodeScanIssue, SecurityTreeViewFilterState, severities, Severity } from '../models/model' +import { + CodeIssueGroupingStrategy, + CodeIssueGroupingStrategyState, + CodeScanIssue, + SecurityTreeViewFilterState, + severities, + Severity, +} from '../models/model' import globals from '../../shared/extensionGlobals' import { getLogger } from '../../shared/logger' import { SecurityIssueProvider } from './securityIssueProvider' @@ -34,6 +41,17 @@ export class SecurityIssueTreeViewProvider implements vscode.TreeDataProvider { + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + switch (groupingStrategy) { + case CodeIssueGroupingStrategy.FileLocation: + return this.getChildrenGroupedByFileLocation(element) + case CodeIssueGroupingStrategy.Severity: + default: + return this.getChildrenGroupedBySeverity(element) + } + } + + private getChildrenGroupedBySeverity(element: SecurityViewTreeItem | undefined) { const filterHiddenSeverities = (severity: Severity) => !SecurityTreeViewFilterState.instance.getHiddenSeverities().includes(severity) @@ -64,6 +82,27 @@ export class SecurityIssueTreeViewProvider implements vscode.TreeDataProvider + !SecurityTreeViewFilterState.instance.getHiddenSeverities().includes(issue.severity) + + if (element instanceof FileItem) { + return element.issues + .filter(filterHiddenSeverities) + .filter((issue) => issue.visible) + .sort((a, b) => a.startLine - b.startLine) + .map((issue) => new IssueItem(element.filePath, issue)) + } + + const result = this.issueProvider.issues + .filter((group) => group.issues.some(filterHiddenSeverities)) + .filter((group) => group.issues.some((issue) => issue.visible)) + .sort((a, b) => a.filePath.localeCompare(b.filePath)) + .map((group) => new FileItem(group.filePath, group.issues.filter(filterHiddenSeverities))) + this._onDidChangeTreeData.fire(result) + return result + } + public refresh(): void { this._onDidChangeTreeData.fire() } @@ -118,7 +157,8 @@ export class IssueItem extends vscode.TreeItem { public readonly issue: CodeScanIssue ) { super(issue.title, vscode.TreeItemCollapsibleState.None) - this.description = `${path.basename(this.filePath)} [Ln ${this.issue.startLine + 1}, Col 1]` + this.description = this.getDescription() + this.iconPath = this.getSeverityIcon() this.tooltip = this.getTooltipMarkdown() this.command = { title: 'Focus Issue', @@ -132,6 +172,22 @@ export class IssueItem extends vscode.TreeItem { return globals.context.asAbsolutePath(`resources/images/severity-${this.issue.severity.toLowerCase()}.svg`) } + private getSeverityIcon() { + const iconPath = globals.context.asAbsolutePath( + `resources/icons/aws/amazonq/severity-${this.issue.severity.toLowerCase()}.svg` + ) + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + return groupingStrategy !== CodeIssueGroupingStrategy.Severity ? iconPath : undefined + } + + private getDescription() { + const positionStr = `[Ln ${this.issue.startLine + 1}, Col 1]` + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + return groupingStrategy !== CodeIssueGroupingStrategy.FileLocation + ? `${path.basename(this.filePath)} ${positionStr}` + : positionStr + } + private getContextValue() { return this.issue.suggestedFixes.length === 0 || !this.issue.suggestedFixes[0].code ? ContextValue.ISSUE_WITHOUT_FIX diff --git a/packages/core/src/codewhisperer/ui/prompters.ts b/packages/core/src/codewhisperer/ui/prompters.ts new file mode 100644 index 00000000000..95541d84a82 --- /dev/null +++ b/packages/core/src/codewhisperer/ui/prompters.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + codeIssueGroupingStrategies, + CodeIssueGroupingStrategy, + codeIssueGroupingStrategyLabel, + CodeIssueGroupingStrategyState, +} from '../models/model' +import { createQuickPick, QuickPickPrompter } from '../../shared/ui/pickerPrompter' +import { localize } from '../../shared/utilities/vsCodeUtils' + +export function createCodeIssueGroupingStrategyPrompter(): QuickPickPrompter { + const groupingStrategy = CodeIssueGroupingStrategyState.instance.getState() + const prompter = createQuickPick( + codeIssueGroupingStrategies.map((strategy) => ({ + data: strategy, + label: codeIssueGroupingStrategyLabel[strategy], + })), + { + title: localize('AWS.amazonq.scans.groupIssues', 'Group Issues'), + placeholder: localize('AWS.amazonq.scans.groupIssues.placeholder', 'Select how to group code issues'), + } + ) + prompter.quickPick.activeItems = prompter.quickPick.items.filter((item) => item.data === groupingStrategy) + prompter.quickPick.onDidChangeSelection(async (items) => { + const [item] = items + await CodeIssueGroupingStrategyState.instance.setState(item.data) + prompter.quickPick.hide() + }) + return prompter +} diff --git a/packages/core/src/shared/globalState.ts b/packages/core/src/shared/globalState.ts index 645929b918f..defb7658f68 100644 --- a/packages/core/src/shared/globalState.ts +++ b/packages/core/src/shared/globalState.ts @@ -32,6 +32,7 @@ export type globalKey = | 'aws.amazonq.hasShownWalkthrough' | 'aws.amazonq.showTryChatCodeLens' | 'aws.amazonq.securityIssueFilters' + | 'aws.amazonq.codescan.groupingStrategy' | 'aws.amazonq.notifications' | 'aws.amazonq.welcomeChatShowCount' | 'aws.amazonq.disclaimerAcknowledged' diff --git a/packages/core/src/test/index.ts b/packages/core/src/test/index.ts index 282fdac2bfc..7e6d500a30c 100644 --- a/packages/core/src/test/index.ts +++ b/packages/core/src/test/index.ts @@ -24,3 +24,4 @@ export * from './credentials/testUtil' export * from './testUtil' export * from './amazonqFeatureDev/utils' export * from './fake/mockFeatureConfigData' +export * from './shared/ui/testUtils'