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 17 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
215 changes: 215 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,215 @@
package io.snyk.plugin.ui.jcef

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.jcef.JBCefBrowserBase
import com.intellij.ui.jcef.JBCefJSQuery
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.cef.browser.CefBrowser
import org.cef.browser.CefFrame
import org.cef.handler.CefLoadHandlerAdapter

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) {

fun generateApplyFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter {
val applyFixQuery = JBCefJSQuery.create(jbCefBrowser)

applyFixQuery.addHandler { value ->
val params = value.split(":")
val filePath = params[0] // Path to the file that needs to be patched
val patch = params[1] // The patch we received from LS

println("[applyFixHandler] Received request to apply fix on file: $filePath")
println("[applyFixHandler] Patch to apply: $patch")
acke marked this conversation as resolved.
Show resolved Hide resolved


// Avoid blocking the UI thread
CoroutineScope(Dispatchers.IO).launch {
try {
val success = applyPatchAndSave(project, filePath, patch)

val script = """
window.receiveApplyFixResponse($success);
""".trimIndent()

withContext(Dispatchers.Main) {
jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0)
}
} catch (e: Exception) {
acke marked this conversation as resolved.
Show resolved Hide resolved
e.printStackTrace()
}
}
return@addHandler JBCefJSQuery.Response("success")
}
acke marked this conversation as resolved.
Show resolved Hide resolved

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): Boolean {
val virtualFile = findVirtualFile(filePath) ?: run {
println("[applyPatchAndSave] Virtual file not found for path: $filePath")
return false
}

println("[applyPatchAndSave] Found virtual file: $virtualFile")
acke marked this conversation as resolved.
Show resolved Hide resolved

val fileContent = ApplicationManager.getApplication().runReadAction<String?> {
val document = FileDocumentManager.getInstance().getDocument(virtualFile)
document?.text
} ?: run {
println("[applyPatchAndSave] Document not found or is null for virtual file: $filePath")
return false
}

println("[applyPatchAndSave] Initial file content: $fileContent")
acke marked this conversation as resolved.
Show resolved Hide resolved

val diffPatch = parseDiff(patch)
val patchedContent = applyPatch(fileContent, diffPatch)

println("[applyPatchAndSave] Patched content that will be written: $patchedContent")
acke marked this conversation as resolved.
Show resolved Hide resolved

// Apply the patch inside a WriteCommandAction
WriteCommandAction.runWriteCommandAction(project) {
val document = FileDocumentManager.getInstance().getDocument(virtualFile)
document?.let {
it.setText(patchedContent)

println("[applyPatchAndSave] Content after applying patch: ${it.text}")
acke marked this conversation as resolved.
Show resolved Hide resolved

FileDocumentManager.getInstance().saveDocument(it)
acke marked this conversation as resolved.
Show resolved Hide resolved
println("[applyPatchAndSave] Patch applied successfully!")
} ?: run {
acke marked this conversation as resolved.
Show resolved Hide resolved
println("[applyPatchAndSave] Failed to find document for saving patched content.")
}
}

return true
}

fun applyPatch(fileContent: String, diffPatch: DiffPatch): String {
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
)
}

private fun findVirtualFile(filePath: String): VirtualFile? {
return LocalFileSystem.getInstance().findFileByPath(filePath)
acke marked this conversation as resolved.
Show resolved Hide resolved
}
}
78 changes: 78 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,78 @@
package io.snyk.plugin.ui.jcef

import com.google.gson.Gson
import com.intellij.openapi.project.Project
import com.intellij.ui.jcef.JBCefBrowserBase
import com.intellij.ui.jcef.JBCefJSQuery
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.cef.browser.CefBrowser
import org.cef.browser.CefFrame
import org.cef.handler.CefLoadHandlerAdapter
import snyk.common.lsp.LanguageServerWrapper

class GenerateAIFixHandler(private val project: Project) {

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]

// Avoids blocking the UI thread
CoroutineScope(Dispatchers.IO).launch {
acke marked this conversation as resolved.
Show resolved Hide resolved
try {
val responseDiff: List<LanguageServerWrapper.Fix> =
LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID)

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

withContext(Dispatchers.Main) {
acke marked this conversation as resolved.
Show resolved Hide resolved
jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0)
JBCefJSQuery.Response("success")
}
} catch (e: Exception) {
e.printStackTrace()
acke marked this conversation as resolved.
Show resolved Hide resolved
}
}
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 @@ -3,8 +3,10 @@ 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.JBColor
import com.intellij.ui.jcef.JBCefBrowserBase
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.PlatformColors
import com.intellij.util.ui.UIUtil
import org.cef.browser.CefBrowser
import org.cef.browser.CefFrame
Expand Down Expand Up @@ -63,8 +65,8 @@ class ThemeBasedStylingGenerator {
val textColor = toCssHex(JBUI.CurrentTheme.Label.foreground())
val linkColor = toCssHex(JBUI.CurrentTheme.Link.Foreground.ENABLED)
val dataFlowColor = toCssHex(baseColor)
val editorColor = toCssHex(baseColor)
val borderColor = toCssHex(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground())
val editorColor = toCssHex(UIUtil.getTextFieldBackground())
val labelColor = toCssHex(JBUI.CurrentTheme.Label.foreground())

val globalScheme = EditorColorsManager.getInstance().globalScheme
Expand Down Expand Up @@ -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': "$linkColor",
'--dark-button-border-default': "$borderColor",
'--dark-button-default': "$linkColor",

};
for (let [property, value] of Object.entries(properties)) {
document.documentElement.style.setProperty(property, value);
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ object JCEFUtils {
val cefClient = JBCefApp.getInstance().createClient()
cefClient.setProperty("JS_QUERY_POOL_SIZE", 1)
val jbCefBrowser =
JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(false)
JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(true)
acke marked this conversation as resolved.
Show resolved Hide resolved
.setMouseWheelEventEnable(true).build()
jbCefBrowser.setOpenLinksInExternalBrowser(true)

Expand Down
Loading
Loading