Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display AI Fix ⚡️ [IDE-580] #596

Merged
merged 28 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1ea7c69
wip: enable AI Fix
Aug 28, 2024
73b5081
feat: wip - bridge button action with LS wrapper
Sep 3, 2024
5563b32
feat: get Code Diff from LS
Sep 3, 2024
b156653
debug: test Language Server response
Sep 4, 2024
575fa56
fix: wip parsing of command
bastiandoetsch Sep 4, 2024
7421d80
feat: display AI suggestions ⚡️
Sep 4, 2024
ff1400b
chore: update `Apply fix` button style
Sep 4, 2024
8a4b271
chore: group similar JS logic
Sep 4, 2024
e03954c
fix: responsive UI during AI Fix call with LS async request
Sep 4, 2024
6c0b5fc
feat: wip - apply fix
Sep 4, 2024
281670a
fix: added support for navigating between AI Fixes
acke Sep 5, 2024
c987b11
feat: show section for error, if ai fix reqest fails to generate fixes
acke Sep 5, 2024
ddea6a7
fix: add the same spacing for error section as for show fixes section
acke Sep 5, 2024
dbaacd1
chore: wip - apply fix
Sep 5, 2024
d0937ba
chore: wip add test to handle patches
Sep 6, 2024
8a387b6
fix: fix flow for regenerating ai fixes after error
acke Sep 6, 2024
e0fc1d2
tidy: replace hardcoded colors with JBUI theme colors
acke Sep 11, 2024
adaca2a
tidy: remove println's and disable devtools
acke Sep 12, 2024
097875a
chore: updating logging and error handling, changing threading to run…
acke Sep 13, 2024
16ae0a1
fix: solved a problem with applying patch
acke Sep 16, 2024
4387f5d
chore: replace colors with colors from JBUI
acke Sep 16, 2024
cc02eb7
tidy: cleanup code, remove temporary logging
acke Sep 16, 2024
a174e45
fix: update unit test and remove duplicate code
acke Sep 16, 2024
ee0b923
fix: improve error handling and added balloon notifier for failing to…
acke Sep 16, 2024
280a979
tidy: refactor functions to a new patcher
acke Sep 16, 2024
f155b48
tidy: remove unused parameters
acke Sep 16, 2024
69e7c1e
fix: updating logging and refactor types
acke Sep 19, 2024
2d00d29
fix: add balloon notifier for file not found
acke Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/main/kotlin/io/snyk/plugin/DiffPatcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.snyk.plugin

import io.snyk.plugin.ui.jcef.Change
import io.snyk.plugin.ui.jcef.DiffPatch
import io.snyk.plugin.ui.jcef.Hunk

class DiffPatcher {

fun applyPatch(fileContent: String, diffPatch: DiffPatch): String {
acke marked this conversation as resolved.
Show resolved Hide resolved
val lines = fileContent.lines().toMutableList()

for (hunk in diffPatch.hunks) {
var originalLineIndex = hunk.startLineOriginal - 1 // Convert to 0-based index

for (change in hunk.changes) {
when (change) {
is Change.Addition -> {
lines.add(originalLineIndex, change.line)
originalLineIndex++
}

is Change.Deletion -> {
if (originalLineIndex < lines.size && lines[originalLineIndex].trim() == change.line) {
lines.removeAt(originalLineIndex)
}
}

is Change.Context -> {
originalLineIndex++ // Move past unchanged context lines
}
}
}
}
return lines.joinToString("\n")
}

fun parseDiff(diff: String): DiffPatch {
val lines = diff.lines()
val originalFile = lines.first { it.startsWith("---") }.substringAfter("--- ")
val fixedFile = lines.first { it.startsWith("+++") }.substringAfter("+++ ")

val hunks = mutableListOf<Hunk>()
var currentHunk: Hunk? = null
val changes = mutableListOf<Change>()

for (line in lines) {
when {
line.startsWith("@@") -> {
// Parse hunk header (e.g., @@ -4,9 +4,14 @@)
val hunkHeader = line.substringAfter("@@ ").substringBefore(" @@").split(" ")
val original = hunkHeader[0].substring(1).split(",")
val fixed = hunkHeader[1].substring(1).split(",")

val startLineOriginal = original[0].toInt()
val numLinesOriginal = original.getOrNull(1)?.toInt() ?: 1
val startLineFixed = fixed[0].toInt()
val numLinesFixed = fixed.getOrNull(1)?.toInt() ?: 1

if (currentHunk != null) {
hunks.add(currentHunk.copy(changes = changes.toList()))
changes.clear()
}
currentHunk = Hunk(
startLineOriginal = startLineOriginal,
numLinesOriginal = numLinesOriginal,
startLineFixed = startLineFixed,
numLinesFixed = numLinesFixed,
changes = emptyList()
)
}

line.startsWith("---") || line.startsWith("+++") -> {
// Skip file metadata lines (--- and +++)
continue
}

line.startsWith("-") -> changes.add(Change.Deletion(line.substring(1).trim()))
line.startsWith("+") -> changes.add(Change.Addition(line.substring(1).trim()))
else -> changes.add(Change.Context(line.trim()))
}
}

// Add the last hunk
if (currentHunk != null) {
hunks.add(currentHunk.copy(changes = changes.toList()))
}

return DiffPatch(
originalFile = originalFile,
fixedFile = fixedFile,
hunks = hunks
)
}
}
138 changes: 138 additions & 0 deletions src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package io.snyk.plugin.ui.jcef

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.ui.jcef.JBCefBrowserBase
import com.intellij.ui.jcef.JBCefJSQuery
import io.snyk.plugin.DiffPatcher
import io.snyk.plugin.toVirtualFile
import org.cef.browser.CefBrowser
import org.cef.browser.CefFrame
import org.cef.handler.CefLoadHandlerAdapter
import org.jetbrains.concurrency.runAsync
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import java.io.IOException


data class DiffPatch(
val originalFile: String,
val fixedFile: String,
val hunks: List<Hunk>
)

data class Hunk(
val startLineOriginal: Int,
val numLinesOriginal: Int,
val startLineFixed: Int,
val numLinesFixed: Int,
val changes: List<Change>
)

sealed class Change {
acke marked this conversation as resolved.
Show resolved Hide resolved
data class Addition(val line: String) : Change()
data class Deletion(val line: String) : Change()
data class Context(val line: String) : Change() // Unchanged line for context
}

class ApplyFixHandler(private val project: Project) {

private val enableDebug = Logger.getInstance("Snyk Language Server").isDebugEnabled
private val enableTrace = Logger.getInstance("Snyk Language Server").isTraceEnabled
private val logger = Logger.getInstance(this::class.java)
acke marked this conversation as resolved.
Show resolved Hide resolved


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

// Avoid blocking the UI thread
runAsync {
//var success = true
acke marked this conversation as resolved.
Show resolved Hide resolved
val result = try {
applyPatchAndSave(project, filePath, patch)
} catch (e: IOException) { // Catch specific file-related exceptions
log("Error applying patch to file: $filePath. e:$e")
Result.failure(e)
} catch (e: Exception) {
log("Unexpected error applying patch. e:$e")
Result.failure(e)
}

ApplicationManager.getApplication().invokeLater {
acke marked this conversation as resolved.
Show resolved Hide resolved
if (result.isSuccess) {
val script = """
window.receiveApplyFixResponse(true);
""".trimIndent()
jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0)
} else {
val errorMessage = "Error applying fix: ${result.exceptionOrNull()?.message}"
SnykBalloonNotificationHelper.showError(errorMessage, project)
val errorScript = """
window.receiveApplyFixResponse(false, "$errorMessage");
""".trimIndent()
jbCefBrowser.cefBrowser.executeJavaScript(errorScript, jbCefBrowser.cefBrowser.url, 0)
}
}
}
return@addHandler JBCefJSQuery.Response("success")
}

return object : CefLoadHandlerAdapter() {
override fun onLoadEnd(browser: CefBrowser, frame: CefFrame, httpStatusCode: Int) {
if (frame.isMain) {
val script = """
(function() {
if (window.applyFixQuery) {
return;
}
window.applyFixQuery = function(value) { ${applyFixQuery.inject("value")} };
})();
""".trimIndent()
browser.executeJavaScript(script, browser.url, 0)
}
}
}
}

private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Result<Unit> {
val virtualFile = filePath.toVirtualFile()
val patcher = DiffPatcher()

return try {
WriteCommandAction.runWriteCommandAction(project) {
val document = FileDocumentManager.getInstance().getDocument(virtualFile)
if (document != null) {
val originalContent = document.text
val patchedContent = patcher.applyPatch(originalContent, patcher.parseDiff(patch))
if (originalContent != patchedContent) {
document.setText(patchedContent)
} else {
log("[applyPatchAndSave] Patch did not modify content: $filePath")
acke marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
log("[applyPatchAndSave] Failed to find document for: $filePath")
return@runWriteCommandAction
}
}
Result.success(Unit)
} catch (e: Exception) {
acke marked this conversation as resolved.
Show resolved Hide resolved
log("[applyPatchAndSave] Error applying patch to: $filePath. e: $e")
Result.failure(e)
}
}

private fun log(logMessage: String) {
acke marked this conversation as resolved.
Show resolved Hide resolved
when {
enableDebug -> logger.debug(logMessage)
acke marked this conversation as resolved.
Show resolved Hide resolved
enableTrace -> logger.trace(logMessage)
else -> logger.error(logMessage)
}
}
}
68 changes: 68 additions & 0 deletions src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.snyk.plugin.ui.jcef

import com.google.gson.Gson
import com.intellij.ui.jcef.JBCefBrowserBase
import com.intellij.ui.jcef.JBCefJSQuery
import org.cef.browser.CefBrowser
import org.cef.browser.CefFrame
import org.cef.handler.CefLoadHandlerAdapter
import org.jetbrains.concurrency.runAsync
import snyk.common.lsp.LanguageServerWrapper

class GenerateAIFixHandler() {

fun generateAIFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter {
val aiFixQuery = JBCefJSQuery.create(jbCefBrowser)

aiFixQuery.addHandler { value ->
val params = value.split(":")
val folderURI = params[0]
val fileURI = params[1]
val issueID = params[2]


runAsync {
val responseDiff: List<LanguageServerWrapper.Fix> =
LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID)

val script = """
window.receiveAIFixResponse(${Gson().toJson(responseDiff)});
""".trimIndent()

jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0)
JBCefJSQuery.Response("success")
}
return@addHandler JBCefJSQuery.Response("success")
}

return object : CefLoadHandlerAdapter() {
override fun onLoadEnd(browser: CefBrowser, frame: CefFrame, httpStatusCode: Int) {
if (frame.isMain) {
val script = """
(function() {
if (window.aiFixQuery) {
return;
}
window.aiFixQuery = function(value) { ${aiFixQuery.inject("value")} };

const aiFixButton = document.getElementById('generate-ai-fix');
const retryFixButton = document.getElementById('retry-generate-fix');
const issueId = aiFixButton.getAttribute('issue-id');
const folderPath = aiFixButton.getAttribute('folder-path');
const filePath = aiFixButton.getAttribute('file-path');

aiFixButton.addEventListener('click', () => {
window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId);
});

retryFixButton.addEventListener('click', () => {
window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId);
});
})();
"""
browser.executeJavaScript(script, browser.url, 0)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 Down Expand Up @@ -57,20 +58,21 @@ class ThemeBasedStylingGenerator {
httpStatusCode: Int,
) {
if (frame.isMain) {
val baseColor = UIUtil.getTextFieldBackground()
val (addedColor, removedColor) = getCodeDiffColors(baseColor, isHighContrast)
val baseColor = UIUtil.getTextFieldBackground() //TODO Replace with JBUI.CurrentTheme colors
acke marked this conversation as resolved.
Show resolved Hide resolved
val (addedColor, removedColor) = getCodeDiffColors(baseColor, isHighContrast) //TODO Replace with JBUI.CurrentTheme colors
acke marked this conversation as resolved.
Show resolved Hide resolved
val dataFlowColor = toCssHex(baseColor)
val editorColor = toCssHex(baseColor)

val textColor = toCssHex(JBUI.CurrentTheme.Label.foreground())
val textColor = toCssHex(JBUI.CurrentTheme.Tree.FOREGROUND)
val linkColor = toCssHex(JBUI.CurrentTheme.Link.Foreground.ENABLED)
val dataFlowColor = toCssHex(baseColor)
val buttonColor = toCssHex(JBUI.CurrentTheme.Button.defaultButtonColorStart())
val borderColor = toCssHex(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground())
val editorColor = toCssHex(UIUtil.getTextFieldBackground())
val labelColor = toCssHex(JBUI.CurrentTheme.Label.foreground())

val globalScheme = EditorColorsManager.getInstance().globalScheme
val tearLineColor = globalScheme.getColor(ColorKey.find("TEARLINE_COLOR")) // The closest color to target_rgb = (198, 198, 200)
val tabItemHoverColor = globalScheme.getColor(ColorKey.find("INDENT_GUIDE")) // The closest color to target_rgb = RGB (235, 236, 240)
val codeTagBgColor = globalScheme.getColor(EditorColors.GUTTER_BACKGROUND) ?: globalScheme.defaultBackground
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
acke marked this conversation as resolved.
Show resolved Hide resolved

val themeScript = """
(function(){
Expand All @@ -92,7 +94,11 @@ class ThemeBasedStylingGenerator {
'--border-color': "$borderColor",
'--editor-color': "$editorColor",
'--label-color': "'$labelColor'",
'--vulnerability-overview-pre-background-color': "${toCssHex(codeTagBgColor)}",
'--container-background-color': "${toCssHex(codeTagBgColor)}",
'--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
Loading
Loading