Skip to content

Commit f76d69d

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

File tree

15 files changed

+250
-56
lines changed

15 files changed

+250
-56
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
@@ -72,6 +76,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_
7276
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_BAR_ACTIONS
7377
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_CHANGE
7478
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_REMOVE
79+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CODE_REVIEW_FINDINGS_SUFFIX
80+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.DISPLAY_FINDINGS_SUFFIX
7581
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams
7682
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams
7783
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.GET_SERIALIZED_CHAT_REQUEST_METHOD
@@ -108,13 +114,23 @@ import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestA
108114
import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
109115
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable
110116
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable
117+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeLine
118+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanIssue
119+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager
120+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession
121+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Description
122+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.Recommendation
123+
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.SuggestedFix
111124
import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable
125+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
112126
import software.aws.toolkits.jetbrains.settings.MeetQSettings
113127
import software.aws.toolkits.telemetry.MetricResult
114128
import software.aws.toolkits.telemetry.Telemetry
129+
import java.nio.file.Path
115130
import java.util.concurrent.CompletableFuture
116131
import java.util.concurrent.CompletionException
117132
import java.util.function.Function
133+
import kotlin.collections.get
118134

119135
class BrowserConnector(
120136
private val serializer: MessageSerializer = MessageSerializer.getInstance(),
@@ -124,6 +140,7 @@ class BrowserConnector(
124140
val uiReady = CompletableDeferred<Boolean>()
125141
private val chatCommunicationManager = ChatCommunicationManager.getInstance(project)
126142
private val chatAsyncResultManager = ChatAsyncResultManager.getInstance(project)
143+
private val codeScanManager = CodeWhispererCodeScanManager.getInstance(project)
127144

128145
suspend fun connect(
129146
browser: Browser,
@@ -572,6 +589,33 @@ class BrowserConnector(
572589
}
573590
}
574591

592+
data class flareCodeScanIssue(
593+
val startLine: Int,
594+
val endLine: Int,
595+
val comment: String?,
596+
val title: String,
597+
val description: Description,
598+
val detectorId: String,
599+
val detectorName: String,
600+
val findingId: String,
601+
val ruleId: String?,
602+
val relatedVulnerabilities: List<String>,
603+
val severity: String,
604+
val recommendation: Recommendation,
605+
val suggestedFixes: List<SuggestedFix>,
606+
val scanJobId: String,
607+
val language: String,
608+
val autoDetected: Boolean,
609+
val filePath: String,
610+
val findingContext: String
611+
612+
)
613+
614+
data class AggregatedCodeScanIssue(
615+
val filePath: String,
616+
val issues: List<flareCodeScanIssue>
617+
)
618+
575619
private fun showResult(
576620
result: CompletableFuture<String>,
577621
partialResultToken: String,
@@ -585,10 +629,74 @@ class BrowserConnector(
585629
throw error
586630
}
587631
chatCommunicationManager.removePartialChatMessage(partialResultToken)
632+
val params = value?.let { encryptionManager?.decrypt(it) }.orEmpty()
633+
val jsonObject = Gson().fromJson(params, Map::class.java)
634+
val additionalMessages = jsonObject["additionalMessages"] as? MutableList<Map<String, Any>>
635+
val findingsMessage = additionalMessages?.find {message ->
636+
(message["messageId"] as String).endsWith(CODE_REVIEW_FINDINGS_SUFFIX)
637+
|| (message["messageId"] as String).endsWith(DISPLAY_FINDINGS_SUFFIX)}
638+
val scannedFiles = mutableListOf<VirtualFile>();
639+
if (findingsMessage != null) {
640+
additionalMessages.remove(findingsMessage)
641+
val gson = Gson()
642+
val jsonFindings = gson.fromJson(findingsMessage["body"] as String, List::class.java)
643+
val mappedFindings = mutableListOf<CodeWhispererCodeScanIssue>()
644+
for (aggregatedIssueUnformatted in jsonFindings) {
645+
val aggregatedIssue = gson.fromJson(gson.toJson(aggregatedIssueUnformatted), AggregatedCodeScanIssue::class.java)
646+
val file = try {
647+
LocalFileSystem.getInstance().findFileByIoFile(
648+
Path.of(aggregatedIssue.filePath).toFile()
649+
)
650+
} catch (e: Exception) {
651+
null
652+
}
653+
if (file?.isDirectory == false) {
654+
scannedFiles.add(file)
655+
runReadAction {
656+
FileDocumentManager.getInstance().getDocument(file)
657+
}?.let { document ->
658+
for (issue in aggregatedIssue.issues) {
659+
val endLineInDocument = minOf(maxOf(0, issue.endLine - 1), document.lineCount - 1)
660+
val endCol = document.getLineEndOffset(endLineInDocument) - document.getLineStartOffset(endLineInDocument) + 1
661+
val isIssueIgnored = codeScanManager.isIgnoredIssue(issue.title, document, file, issue.startLine - 1)
662+
if (isIssueIgnored) {
663+
continue
664+
}
665+
mappedFindings.add(CodeWhispererCodeScanIssue(
666+
startLine = issue.startLine,
667+
startCol = 1,
668+
endLine = issue.endLine,
669+
endCol = endCol,
670+
file = file,
671+
project = project,
672+
title = issue.title,
673+
description = issue.description,
674+
detectorId = issue.detectorId,
675+
detectorName = issue.detectorName,
676+
findingId = issue.findingId,
677+
ruleId = issue.ruleId,
678+
relatedVulnerabilities = issue.relatedVulnerabilities,
679+
severity = issue.severity,
680+
recommendation = issue.recommendation,
681+
suggestedFixes = issue.suggestedFixes,
682+
codeSnippet = emptyList(),
683+
isVisible = true,
684+
autoDetected = issue.autoDetected,
685+
scanJobId = issue.scanJobId
686+
))
687+
}
688+
}
689+
}
690+
}
691+
692+
codeScanManager.addOnDemandIssues(mappedFindings, scannedFiles, CodeWhispererConstants.CodeAnalysisScope.AGENTIC)
693+
codeScanManager.showCodeScanUI()
694+
}
695+
588696
val messageToChat = ChatCommunicationManager.convertToJsonToSendToChat(
589697
SEND_CHAT_COMMAND_PROMPT,
590698
tabId,
591-
value?.let { encryptionManager?.decrypt(it) }.orEmpty(),
699+
Gson().toJson(jsonObject),
592700
isPartialResult = false
593701
)
594702
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)