diff --git a/CHANGELOG.md b/CHANGELOG.md index a84c21128..909145a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - add option in IntelliJ registry to display tooltips with issue information - display documentation info when hovering over issue - add inline value support for display of vulnerability count +- added ai fix feedback support ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index c397ecd1f..c4a2fc8f9 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -26,14 +26,14 @@ class ApplyFixHandler(private val project: Project) { if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) } - fun generateApplyFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { val applyFixQuery = JBCefJSQuery.create(jbCefBrowser) applyFixQuery.addHandler { value -> - val params = value.split("|@", limit = 2) - val filePath = params[0] // Path to the file that needs to be patched - val patch = params[1] // The patch we received from LS + val params = value.split("|@", limit = 3) + val fixId = params[0] // Path to the file that needs to be patched + val filePath = params[1] // Path to the file that needs to be patched + val patch = params[2] // The patch we received from LS // Avoid blocking the UI thread runAsync { @@ -52,6 +52,7 @@ class ApplyFixHandler(private val project: Project) { window.receiveApplyFixResponse(true); """.trimIndent() jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) + LanguageServerWrapper.getInstance().submitAutofixFeedbackCommand(fixId, "FIX_APPLIED") } else { val errorMessage = "Error applying fix: ${result.exceptionOrNull()?.message}" SnykBalloonNotificationHelper.showError(errorMessage, project) @@ -60,8 +61,8 @@ class ApplyFixHandler(private val project: Project) { """.trimIndent() jbCefBrowser.cefBrowser.executeJavaScript(errorScript, jbCefBrowser.cefBrowser.url, 0) } - } + return@addHandler JBCefJSQuery.Response("success") } @@ -103,6 +104,7 @@ class ApplyFixHandler(private val project: Project) { return@runWriteCommandAction } } + return Result.success(Unit) } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt index d430c2ae7..8f5404663 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -60,6 +60,10 @@ class GenerateAIFixHandler() { retryFixButton.addEventListener('click', () => { window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); }); + + retryFixButton.addEventListener('click', () => { + window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); + }); })(); """ browser.executeJavaScript(script, browser.url, 0) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt index 40eca090d..3398e8317 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt @@ -1,7 +1,6 @@ package io.snyk.plugin.ui.jcef import com.intellij.openapi.editor.colors.ColorKey -import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.ui.jcef.JBCefBrowserBase import com.intellij.util.ui.JBUI @@ -20,34 +19,6 @@ class ThemeBasedStylingGenerator { } } - fun shift( - colorComponent: Int, - d: Double, - ): Int { - val n = (colorComponent * d).toInt() - return n.coerceIn(0, 255) - } - - fun getCodeDiffColors( - baseColor: Color, - isHighContrast: Boolean, - ): Pair { - val addedColor = - if (isHighContrast) { - Color(28, 68, 40) // high contrast green - } else { - Color(shift(baseColor.red, 0.75), baseColor.green, shift(baseColor.blue, 0.75)) - } - - val removedColor = - if (isHighContrast) { - Color(84, 36, 38) // high contrast red - } else { - Color(shift(baseColor.red, 1.25), shift(baseColor.green, 0.85), shift(baseColor.blue, 0.85)) - } - return Pair(addedColor, removedColor) - } - @Suppress("UNUSED_PARAMETER") fun generate(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { val isDarkTheme = EditorColorsManager.getInstance().isDarkEditor @@ -61,21 +32,22 @@ class ThemeBasedStylingGenerator { httpStatusCode: Int, ) { if (frame.isMain) { - val baseColor = UIUtil.getTextFieldBackground() //TODO Replace with JBUI.CurrentTheme colors - val (addedColor, removedColor) = getCodeDiffColors(baseColor, isHighContrast) //TODO Replace with JBUI.CurrentTheme colors - val dataFlowColor = toCssHex(baseColor) - val editorColor = toCssHex(baseColor) - val textColor = toCssHex(JBUI.CurrentTheme.Tree.FOREGROUND) val linkColor = toCssHex(JBUI.CurrentTheme.Link.Foreground.ENABLED) - val buttonColor = toCssHex(JBUI.CurrentTheme.Button.defaultButtonColorStart()) + val borderColor = toCssHex(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()) val labelColor = toCssHex(JBUI.CurrentTheme.Label.foreground()) + val background = toCssHex(JBUI.CurrentTheme.Tree.BACKGROUND) + val issuePanelBackground = toCssHex(JBUI.CurrentTheme.DefaultTabs.background()) + val tabUnderline = toCssHex(JBUI.CurrentTheme.DefaultTabs.underlineColor()) + val redCodeBlock = toCssHex(JBUI.CurrentTheme.Banner.ERROR_BORDER_COLOR) + val greenCodeBlock = toCssHex(JBUI.CurrentTheme.Banner.SUCCESS_BORDER_COLOR) + val aiCodeBg = UIUtil.getTextFieldBackground() + val codeBlockText = toCssHex(JBUI.CurrentTheme.Tree.FOREGROUND) + val buttonColor = toCssHex(JBUI.CurrentTheme.Button.defaultButtonColorStart()) val globalScheme = EditorColorsManager.getInstance().globalScheme val tearLineColor = globalScheme.getColor(ColorKey.find("TEARLINE_COLOR")) //TODO Replace with JBUI.CurrentTheme colors - val tabItemHoverColor = globalScheme.getColor(ColorKey.find("INDENT_GUIDE")) //TODO Replace with JBUI.CurrentTheme colors - val codeTagBgColor = globalScheme.getColor(EditorColors.GUTTER_BACKGROUND) ?: globalScheme.defaultBackground //TODO Replace with JBUI.CurrentTheme colors val themeScript = """ (function(){ @@ -85,23 +57,22 @@ class ThemeBasedStylingGenerator { window.themeApplied = true; const style = getComputedStyle(document.documentElement); const properties = { - '--text-color': "$textColor", + '--text-color': "$codeBlockText", '--link-color': "$linkColor", - '--data-flow-body-color': "$dataFlowColor", - '--example-line-added-color': "${toCssHex(addedColor)}", - '--example-line-removed-color': "${toCssHex(removedColor)}", + '--data-flow-body-color': "$background", + '--example-line-added-color': "$greenCodeBlock", + '--example-line-removed-color': "$redCodeBlock", '--tab-item-github-icon-color': "$textColor", - '--tab-item-hover-color': "${tabItemHoverColor?.let { toCssHex(it) }}", + '--tab-item-hover-color': "$tabUnderline", '--scrollbar-thumb-color': "${tearLineColor?.let { toCssHex(it) }}", - '--tabs-bottom-color': "${tearLineColor?.let { toCssHex(it) }}", + '--tabs-bottom-color': "$issuePanelBackground", '--border-color': "$borderColor", - '--editor-color': "$editorColor", + '--editor-color': "${toCssHex(aiCodeBg)}", '--label-color': "'$labelColor'", - '--container-background-color': "${toCssHex(codeTagBgColor)}", + '--container-background-color': "${toCssHex(aiCodeBg)}", '--generated-ai-fix-button-background-color': "$buttonColor", '--dark-button-border-default': "$borderColor", '--dark-button-default': "$buttonColor", - }; for (let [property, value] of Object.entries(properties)) { document.documentElement.style.setProperty(property, value); diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index cf747bdf8..1c7fc2ded 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -323,11 +323,12 @@ class SuggestionDescriptionPanelFromLS( if (!fixes.length) return; const currentFix = fixes[diffSelectedIndex]; + const fixId = currentFix.fixId; const filePath = getFilePathFromFix(currentFix); const patch = currentFix.unifiedDiffsPerFile[filePath]; - window.applyFixQuery(filePath + '|@' + patch); + window.applyFixQuery(fixId + '|@' + filePath + '|@' + patch); // Following VSCode logic, the steps are: // 1. Read the current file content. diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index e1b79c4cd..52b94f946 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -1,6 +1,7 @@ package snyk.common.lsp import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service @@ -48,6 +49,7 @@ import org.jetbrains.concurrency.runAsync import snyk.common.EnvironmentHelper import snyk.common.getEndpointUrl import snyk.common.lsp.commands.COMMAND_CODE_FIX_DIFFS +import snyk.common.lsp.commands.COMMAND_CODE_SUBMIT_FIX_FEEDBACK import snyk.common.lsp.commands.COMMAND_COPY_AUTH_LINK import snyk.common.lsp.commands.COMMAND_EXECUTE_CLI import snyk.common.lsp.commands.COMMAND_GET_ACTIVE_USER @@ -558,6 +560,20 @@ class LanguageServerWrapper( } + fun submitAutofixFeedbackCommand(fixId: String, feedback: String) { + if (!ensureLanguageServerInitialized()) return + + try { + val param = ExecuteCommandParams() + param.command = COMMAND_CODE_SUBMIT_FIX_FEEDBACK + param.arguments = listOf(fixId, feedback) + languageServer.workspaceService.executeCommand(param) + } catch (err: Exception) { + logger.warn("Error in submitAutofixFeedbackCommand", err) + } + } + + private fun ensureLanguageServerProtocolVersion(project: Project) { val protocolVersion = initializeResult?.serverInfo?.version pluginSettings().currentLSProtocolVersion = protocolVersion?.toIntOrNull() diff --git a/src/main/kotlin/snyk/common/lsp/commands/Commands.kt b/src/main/kotlin/snyk/common/lsp/commands/Commands.kt index 854c789bd..3a8affa41 100644 --- a/src/main/kotlin/snyk/common/lsp/commands/Commands.kt +++ b/src/main/kotlin/snyk/common/lsp/commands/Commands.kt @@ -10,3 +10,4 @@ internal const val COMMAND_LOGOUT = "snyk.logout" internal const val COMMAND_GET_SETTINGS_SAST_ENABLED = "snyk.getSettingsSastEnabled" internal const val COMMAND_COPY_AUTH_LINK = "snyk.copyAuthLink" internal const val COMMAND_CODE_FIX_DIFFS = "snyk.code.fixDiffs" +internal const val COMMAND_CODE_SUBMIT_FIX_FEEDBACK = "snyk.code.submitFixFeedback" diff --git a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index cd5d769f5..03992e669 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -1,9 +1,6 @@ package snyk.container import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.application.invokeAndWaitIfNeeded -import com.intellij.openapi.application.invokeLater import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileDocumentManager @@ -32,7 +29,6 @@ import java.io.File import java.nio.file.Files import java.nio.file.LinkOption import java.nio.file.Paths -import java.time.Duration import java.util.concurrent.TimeUnit import kotlin.io.path.notExists @@ -81,7 +77,11 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) } PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - await().timeout(5, TimeUnit.SECONDS).until { virtualFile?.isValid ?: false } + + await().timeout(300, TimeUnit.SECONDS).until { + println(System.currentTimeMillis()) + virtualFile?.isValid ?: false + } ApplicationManager.getApplication().runWriteAction {