Skip to content

Commit

Permalink
feat: ai fix feedback [IDE-634] (#615)
Browse files Browse the repository at this point in the history
* feat: get Code Diff from LS

* debug: test Language Server response

* feat: display AI suggestions ⚡️

* fix: responsive UI during AI Fix call with LS async request

* fix: added support for navigating between AI Fixes

* fix: fix flow for regenerating ai fixes after error

* tidy: replace hardcoded colors with JBUI theme colors

* tidy: remove println's and disable devtools

* chore: updating logging and error handling, changing threading to runAsync

* fix: solved a problem with applying patch

* chore: replace colors with colors from JBUI

* fix: improve error handling and added balloon notifier for failing to apply patch

* tidy: refactor functions to a new patcher

* feat: send ai fix feedback

* chore: clean up color schema, remove unused functions

* chore: update changelog with ai fix feedback

* chore: remove console logging and add error and log  handling

---------

Co-authored-by: Catalina Oyaneder <catalina.oyaneder@snyk.io>
Co-authored-by: Bastian Doetsch <bastian.doetsch@snyk.io>
  • Loading branch information
3 people authored Sep 30, 2024
1 parent 2c09695 commit 7de4b87
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 58 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Snyk Security Changelog


## [2.10.0]
### Changed
- save git folder config in settings
Expand All @@ -17,6 +18,7 @@
- always display info nodes
- add option in IntelliJ registry to display tooltips with issue information
- display documentation info when hovering over issue
- added ai fix feedback support

### Fixes
- add name to code vision provider
Expand Down
12 changes: 7 additions & 5 deletions src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -60,8 +61,8 @@ class ApplyFixHandler(private val project: Project) {
""".trimIndent()
jbCefBrowser.cefBrowser.executeJavaScript(errorScript, jbCefBrowser.cefBrowser.url, 0)
}

}

return@addHandler JBCefJSQuery.Response("success")
}

Expand Down Expand Up @@ -103,6 +104,7 @@ class ApplyFixHandler(private val project: Project) {
return@runWriteCommandAction
}
}

return Result.success(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
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
import com.intellij.ui.JBColor
import com.intellij.util.ui.UIUtil
import org.cef.browser.CefBrowser
import org.cef.browser.CefFrame
Expand All @@ -17,34 +15,6 @@ class ThemeBasedStylingGenerator {
return "#%02x%02x%02x".format(color.red, color.green, color.blue)
}

fun shift(
colorComponent: Int,
d: Double,
): Int {
val n = (colorComponent * d).toInt()
return n.coerceIn(0, 255)
}

fun getCodeDiffColors(
baseColor: Color,
isHighContrast: Boolean,
): Pair<Color, Color> {
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
Expand All @@ -58,21 +28,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(){
Expand All @@ -82,23 +53,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,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.
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -548,6 +550,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()
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/snyk/common/lsp/commands/Commands.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
10 changes: 5 additions & 5 deletions src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 7de4b87

Please sign in to comment.