Skip to content

Commit 99d8433

Browse files
feat(amazonq): add support for CodeReview and DisplayFindings tools, alter behavior of explain and fix buttons
1 parent ce3a4ed commit 99d8433

File tree

15 files changed

+242
-53
lines changed

15 files changed

+242
-53
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ import com.google.gson.Gson
1010
import com.intellij.ide.BrowserUtil
1111
import com.intellij.ide.util.RunOnceUtil
1212
import com.intellij.openapi.application.runInEdt
13+
import com.intellij.openapi.application.runReadAction
14+
import com.intellij.openapi.fileEditor.FileDocumentManager
1315
import com.intellij.openapi.fileEditor.FileEditorManager
1416
import com.intellij.openapi.options.ShowSettingsUtil
1517
import com.intellij.openapi.project.Project
18+
import com.intellij.openapi.vfs.LocalFileSystem
19+
import com.intellij.openapi.vfs.VirtualFile
1620
import com.intellij.ui.jcef.JBCefJSQuery.Response
1721
import kotlinx.coroutines.CancellationException
1822
import kotlinx.coroutines.CompletableDeferred
@@ -71,6 +75,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_
7175
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_BAR_ACTIONS
7276
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_CHANGE
7377
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_REMOVE
78+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CODE_REVIEW_FINDINGS_SUFFIX
79+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DISPLAY_FINDINGS_SUFFIX
7480
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams
7581
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams
7682
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD
@@ -107,13 +113,23 @@ import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestA
107113
import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
108114
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable
109115
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable
116+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeLine
117+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue
118+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
119+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession
120+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Description
121+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Recommendation
122+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.SuggestedFix
110123
import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable
124+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
111125
import software.aws.toolkits.jetbrains.settings.MeetQSettings
112126
import software.aws.toolkits.telemetry.MetricResult
113127
import software.aws.toolkits.telemetry.Telemetry
128+
import java.nio.file.Path
114129
import java.util.concurrent.CompletableFuture
115130
import java.util.concurrent.CompletionException
116131
import java.util.function.Function
132+
import kotlin.collections.get
117133

118134
class BrowserConnector(
119135
private val serializer: MessageSerializer = MessageSerializer.getInstance(),
@@ -122,6 +138,7 @@ class BrowserConnector(
122138
) {
123139
val uiReady = CompletableDeferred<Boolean>()
124140
private val chatCommunicationManager = ChatCommunicationManager.getInstance(project)
141+
private val codeScanManager = CodeWhispererCodeScanManager.getInstance(project)
125142

126143
suspend fun connect(
127144
browser: Browser,
@@ -568,6 +585,33 @@ class BrowserConnector(
568585
}
569586
}
570587

588+
data class flareCodeScanIssue(
589+
val startLine: Int,
590+
val endLine: Int,
591+
val comment: String?,
592+
val title: String,
593+
val description: Description,
594+
val detectorId: String,
595+
val detectorName: String,
596+
val findingId: String,
597+
val ruleId: String?,
598+
val relatedVulnerabilities: List<String>,
599+
val severity: String,
600+
val recommendation: Recommendation,
601+
val suggestedFixes: List<SuggestedFix>,
602+
val scanJobId: String,
603+
val language: String,
604+
val autoDetected: Boolean,
605+
val filePath: String,
606+
val findingContext: String
607+
608+
)
609+
610+
data class AggregatedCodeScanIssue(
611+
val filePath: String,
612+
val issues: List<flareCodeScanIssue>
613+
)
614+
571615
private fun showResult(
572616
result: CompletableFuture<String>,
573617
partialResultToken: String,
@@ -581,10 +625,74 @@ class BrowserConnector(
581625
throw error
582626
}
583627
chatCommunicationManager.removePartialChatMessage(partialResultToken)
628+
val params = value?.let { encryptionManager?.decrypt(it) }.orEmpty()
629+
val jsonObject = Gson().fromJson(params, Map::class.java)
630+
val additionalMessages = jsonObject["additionalMessages"] as? MutableList<Map<String, Any>>
631+
val findingsMessage = additionalMessages?.find {message ->
632+
(message["messageId"] as String).endsWith(CODE_REVIEW_FINDINGS_SUFFIX)
633+
|| (message["messageId"] as String).endsWith(DISPLAY_FINDINGS_SUFFIX)}
634+
val scannedFiles = mutableListOf<VirtualFile>();
635+
if (findingsMessage != null) {
636+
additionalMessages.remove(findingsMessage)
637+
val gson = Gson()
638+
val jsonFindings = gson.fromJson(findingsMessage["body"] as String, List::class.java)
639+
val mappedFindings = mutableListOf<CodeWhispererCodeScanIssue>()
640+
for (aggregatedIssueUnformatted in jsonFindings) {
641+
val aggregatedIssue = gson.fromJson(gson.toJson(aggregatedIssueUnformatted), AggregatedCodeScanIssue::class.java)
642+
val file = try {
643+
LocalFileSystem.getInstance().findFileByIoFile(
644+
Path.of(aggregatedIssue.filePath).toFile()
645+
)
646+
} catch (e: Exception) {
647+
null
648+
}
649+
if (file?.isDirectory == false) {
650+
scannedFiles.add(file)
651+
runReadAction {
652+
FileDocumentManager.getInstance().getDocument(file)
653+
}?.let { document ->
654+
for (issue in aggregatedIssue.issues) {
655+
val endLineInDocument = minOf(maxOf(0, issue.endLine - 1), document.lineCount - 1)
656+
val endCol = document.getLineEndOffset(endLineInDocument) - document.getLineStartOffset(endLineInDocument) + 1
657+
val isIssueIgnored = codeScanManager.isIgnoredIssue(issue.title, document, file, issue.startLine - 1)
658+
if (isIssueIgnored) {
659+
continue
660+
}
661+
mappedFindings.add(CodeWhispererCodeScanIssue(
662+
startLine = issue.startLine,
663+
startCol = 1,
664+
endLine = issue.endLine,
665+
endCol = endCol,
666+
file = file,
667+
project = project,
668+
title = issue.title,
669+
description = issue.description,
670+
detectorId = issue.detectorId,
671+
detectorName = issue.detectorName,
672+
findingId = issue.findingId,
673+
ruleId = issue.ruleId,
674+
relatedVulnerabilities = issue.relatedVulnerabilities,
675+
severity = issue.severity,
676+
recommendation = issue.recommendation,
677+
suggestedFixes = issue.suggestedFixes,
678+
codeSnippet = emptyList(),
679+
isVisible = true,
680+
autoDetected = issue.autoDetected,
681+
scanJobId = issue.scanJobId
682+
))
683+
}
684+
}
685+
}
686+
}
687+
688+
codeScanManager.addOnDemandIssues(mappedFindings, scannedFiles, CodeWhispererConstants.CodeAnalysisScope.AGENTIC)
689+
codeScanManager.showCodeScanUI()
690+
}
691+
584692
val messageToChat = ChatCommunicationManager.convertToJsonToSendToChat(
585693
SEND_CHAT_COMMAND_PROMPT,
586694
tabId,
587-
value?.let { encryptionManager?.decrypt(it) }.orEmpty(),
695+
Gson().toJson(jsonObject),
588696
isPartialResult = false
589697
)
590698
browser.postChat(messageToChat)
Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,42 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_
1818
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendToPromptParams
1919
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TriggerType
2020

21-
class ExplainCodeIssueAction : AnAction(), DumbAware {
21+
class HandleIssueCommandAction : AnAction(), DumbAware {
2222
override fun getActionUpdateThread() = ActionUpdateThread.BGT
2323

2424
override fun update(e: AnActionEvent) {
2525
e.presentation.isEnabledAndVisible = e.project != null
2626
}
27+
fun createLineRangeText(issueContext: MutableMap<String, String>): String {
28+
val startLineString = issueContext["startLine"]
29+
val endLineString = issueContext["endLine"]
30+
val startLineInteger = Integer.parseInt(startLineString)
31+
val endLineInteger = Integer.parseInt(endLineString)
32+
return if (startLineInteger == endLineInteger) {
33+
"[${startLineInteger}]"
34+
} else {
35+
"[${startLineInteger}, ${endLineInteger}]"
36+
}
37+
}
2738

2839
override fun actionPerformed(e: AnActionEvent) {
2940
val project = e.project ?: return
30-
val issueDataKey = DataKey.create<MutableMap<String, String>>("amazonq.codescan.explainissue")
31-
val issueContext = e.getData(issueDataKey) ?: return
32-
41+
val contextDataKey = DataKey.create<MutableMap<String, String>>("amazonq.codescan.handleIssueCommandContext")
42+
val actionDataKey = DataKey.create<String>("amazonq.codescan.handleIssueCommandAction")
43+
val context = e.getData(contextDataKey) ?: return
44+
val action = e.getData(actionDataKey) ?: return
3345
ActionManager.getInstance().getAction("q.openchat").actionPerformed(e)
3446

3547
ApplicationManager.getApplication().executeOnPooledThread {
3648
runBlocking {
3749
// https://github.com/aws/aws-toolkit-vscode/blob/master/packages/amazonq/src/lsp/chat/commands.ts#L30
38-
val codeSelection = "\n```\n${issueContext["code"]?.trimIndent()?.trim()}\n```\n"
50+
val codeSelection = "\n```\n${context["code"]?.trimIndent()?.trim()}\n```\n"
51+
val actionString = if (action == "explainIssue") "Explain" else "Fix"
3952

40-
val prompt = "Explain the issue \n\n " +
41-
"Issue: \"${issueContext["title"]}\" \n" +
42-
"Code: $codeSelection"
53+
val prompt = "$actionString ${context["title"]} issue in ${context["fileName"]} at ${createLineRangeText(context)}"
4354

44-
val modelPrompt = "Explain the issue ${issueContext["title"]} \n\n " +
45-
"Issue: \"${issueContext["title"]}\" \n" +
46-
"Description: ${issueContext["description"]} \n" +
47-
"Code: $codeSelection and generate the code demonstrating the fix"
55+
val modelPrompt = "$actionString ${context["title"]} issue in ${context["fileName"]} at ${createLineRangeText(context)}" +
56+
"Issue: \"${context["issue"]}\" \n"
4857

4958
val params = SendToPromptParams(
5059
selection = codeSelection,

plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@
108108

109109
<group id = "aws.toolkit.explorer.codewhisperer.codescan">
110110
<action
111-
id="aws.amazonq.explainCodeScanIssue"
112-
class="software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions.ExplainCodeIssueAction"/>
111+
id="aws.amazonq.handleCodeScanIssueCommand"
112+
class="software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions.HandleIssueCommandAction"/>
113113
<action
114114
id="aws.amazonq.codeScanComplete"
115115
class="software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions.CodeScanCompleteAction" />

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanIssueDetailsPanel.kt

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import software.aws.toolkits.core.utils.warn
2222
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.context.CodeScanIssueDetailsDisplayType
2323
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.additionBackgroundColor
2424
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.additionForegroundColor
25-
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.applySuggestedFix
25+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.applyFix
2626
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBackgroundColor
2727
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockBorderColor
2828
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.codeBlockForegroundColor
@@ -143,9 +143,10 @@ internal class CodeWhispererCodeScanIssueDetailsPanel(
143143

144144
buttonPane.apply {
145145
removeAll()
146-
if (issue.suggestedFixes.isNotEmpty()) add(applyFixButton)
147-
add(regenerateFixButton)
146+
// if (issue.suggestedFixes.isNotEmpty()) add(applyFixButton)
147+
// add(regenerateFixButton)
148148
add(explainIssueButton)
149+
add(applyFixButton)
149150
add(ignoreIssueButton)
150151
add(ignoreIssuesButton)
151152
add(Box.createHorizontalGlue())
@@ -234,27 +235,32 @@ internal class CodeWhispererCodeScanIssueDetailsPanel(
234235
horizontalTextPosition = JLabel.LEFT
235236
font = font.deriveFont(16f)
236237
}
238+
// private val applyFixButton = JButton(message("codewhisperer.codescan.apply_fix_button_label")).apply {
239+
// putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
240+
// addActionListener {
241+
// applySuggestedFix(project, issue)
242+
// }
243+
// }
244+
// private val generateFixButton = JButton(message("codewhisperer.codescan.generate_fix_button_label")).apply {
245+
// putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
246+
// isEnabled = issue.ruleId != "sbom-software-assurance-services"
247+
// addActionListener {
248+
// defaultScope.launch {
249+
// handleGenerateFix(issue)
250+
// }
251+
// }
252+
// }
253+
// private val regenerateFixButton = JButton(message("codewhisperer.codescan.regenerate_fix_button_label")).apply {
254+
// putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
255+
// addActionListener {
256+
// defaultScope.launch {
257+
// handleGenerateFix(issue, isRegenerate = true)
258+
// }
259+
// }
260+
// }
237261
private val applyFixButton = JButton(message("codewhisperer.codescan.apply_fix_button_label")).apply {
238-
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
239262
addActionListener {
240-
applySuggestedFix(project, issue)
241-
}
242-
}
243-
private val generateFixButton = JButton(message("codewhisperer.codescan.generate_fix_button_label")).apply {
244-
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
245-
isEnabled = issue.ruleId != "sbom-software-assurance-services"
246-
addActionListener {
247-
defaultScope.launch {
248-
handleGenerateFix(issue)
249-
}
250-
}
251-
}
252-
private val regenerateFixButton = JButton(message("codewhisperer.codescan.regenerate_fix_button_label")).apply {
253-
putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true)
254-
addActionListener {
255-
defaultScope.launch {
256-
handleGenerateFix(issue, isRegenerate = true)
257-
}
263+
applyFix(issue)
258264
}
259265
}
260266
private val explainIssueButton = JButton(message("codewhisperer.codescan.explain_button_label")).apply {
@@ -298,9 +304,10 @@ internal class CodeWhispererCodeScanIssueDetailsPanel(
298304
private val buttonPane = JPanel().apply {
299305
layout = BoxLayout(this, BoxLayout.X_AXIS)
300306
preferredSize = Dimension(this.width, 30)
301-
if (issue.suggestedFixes.isNotEmpty()) add(applyFixButton)
302-
if (issue.suggestedFixes.isNotEmpty()) add(regenerateFixButton) else add(generateFixButton)
307+
// if (issue.suggestedFixes.isNotEmpty()) add(applyFixButton)
308+
// if (issue.suggestedFixes.isNotEmpty()) add(regenerateFixButton) else add(generateFixButton)
303309
add(explainIssueButton)
310+
add(applyFixButton)
304311
add(ignoreIssueButton)
305312
add(ignoreIssuesButton)
306313
add(Box.createHorizontalGlue())
@@ -333,10 +340,10 @@ internal class CodeWhispererCodeScanIssueDetailsPanel(
333340
add(BorderLayout.SOUTH, buttonPane)
334341
isVisible = true
335342
revalidate()
336-
if (issue.suggestedFixes.isEmpty()) {
337-
defaultScope.launch {
338-
handleGenerateFix(issue)
339-
}
340-
}
343+
// if (issue.suggestedFixes.isEmpty()) {
344+
// defaultScope.launch {
345+
// handleGenerateFix(issue)
346+
// }
347+
// }
341348
}
342349
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererCodeScanManager.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,15 @@ class CodeWhispererCodeScanManager(val project: Project) {
537537
ondemandScanIssues = ondemandScanIssues.filter { it.findingId != issue.findingId }
538538
}
539539

540+
fun addOnDemandIssues(issues: List<CodeWhispererCodeScanIssue>, scannedFiles: List<VirtualFile>, scope: CodeWhispererConstants.CodeAnalysisScope) = projectCoroutineScope(project).launch {
541+
ondemandScanIssues = ondemandScanIssues + issues
542+
renderResponseOnUIThread(
543+
getCombinedScanIssues(),
544+
scannedFiles,
545+
scope
546+
)
547+
}
548+
540549
fun removeIssueByFindingId(issue: CodeWhispererCodeScanIssue, findingId: String) {
541550
scanNodesLookup[issue.file]?.forEach { node ->
542551
val issueNode = node.userObject as CodeWhispererCodeScanIssue

0 commit comments

Comments
 (0)