Skip to content

Commit

Permalink
feat: display AI suggestions ⚡️
Browse files Browse the repository at this point in the history
  • Loading branch information
Catalina Oyaneder committed Sep 4, 2024
1 parent 575fa56 commit 7421d80
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 123 deletions.
17 changes: 7 additions & 10 deletions src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package io.snyk.plugin.ui.jcef

import com.google.gson.Gson
import com.intellij.openapi.project.Project
import com.intellij.ui.jcef.JBCefBrowserBase
Expand All @@ -16,18 +18,13 @@ class GenerateAIFixHandler(private val project: Project) {
val folderURI = params[0]
val fileURI = params[1]
val issueID = params[2]
JBCefJSQuery.Response("success")

println("Received folderURI: $folderURI, fileURI: $fileURI, issueID: $issueID")

val responseDiff: List<LanguageServerWrapper.Fix> = LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID)
println("Received responseDiff: $responseDiff")
responseDiff.forEach { fix ->
println("Fix: $fix")
}

val responseDiff: List<LanguageServerWrapper.Fix> =
LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID)
val script = """
window.receiveAIFixResponse(${Gson().toJson(responseDiff)});
"""
window.receiveAIFixResponse(${Gson().toJson(responseDiff)});
""".trimIndent()
jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0)
JBCefJSQuery.Response("success")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.snyk.plugin.ui.toolwindow.panels

import GenerateAIFixHandler
import io.snyk.plugin.ui.jcef.GenerateAIFixHandler
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.uiDesigner.core.GridLayoutManager
Expand Down Expand Up @@ -31,9 +31,9 @@ class SuggestionDescriptionPanelFromLS(
snykFile: SnykFile,
private val issue: ScanIssue,
) : IssueDescriptionPanelBase(
title = issue.title(),
severity = issue.getSeverityAsEnum(),
) {
title = issue.title(),
severity = issue.getSeverityAsEnum(),
) {
val project = snykFile.project
private val unexpectedErrorMessage =
"Snyk encountered an issue while rendering the vulnerability description. Please try again, or contact support if the problem persists. We apologize for any inconvenience caused."
Expand Down Expand Up @@ -161,7 +161,7 @@ class SuggestionDescriptionPanelFromLS(

html = html.replace("\${ideStyle}", "<style nonce=\${nonce}>$ideStyle</style>")
html = html.replace("\${headerEnd}", "")
html = html.replace("\${ideScript}" ,"<script nonce=\${nonce}>$ideScript</script>")
html = html.replace("\${ideScript}", "<script nonce=\${nonce}>$ideScript</script>")

val nonce = getNonce()
html = html.replace("\${nonce}", nonce)
Expand All @@ -179,50 +179,119 @@ class SuggestionDescriptionPanelFromLS(
private fun getCustomScript(): String {
return """
(function () {
function toggleElement(element, toggle) {
if (!element) return;
if (toggle === 'show') {
element.classList.remove('hidden');
} else if (toggle === 'hide') {
element.classList.add('hidden');
} else {
console.error('Unexpected toggle value', toggle);
}
function toggleElement(element, toggle) {
if (!element) return;
if (toggle === "show") {
element.classList.remove("hidden");
} else if (toggle === "hide") {
element.classList.add("hidden");
} else {
console.error("Unexpected toggle value", toggle);
}
}
const generateAiFixBtn = document.getElementById('generate-ai-fix');
const loadingIndicator = document.getElementById('fix-loading-indicator');
const fixesSection = document.getElementById('fixes-section');
const diffContainer = document.getElementById('diff');
let diffSelectedIndex = 0;
function generateAIFix() {
toggleElement(generateAiFixBtn, 'hide');
toggleElement(document.getElementById('fix-loading-indicator'), 'show');
}
const generateAiFixBtn = document.getElementById("generate-ai-fix");
const loadingIndicator = document.getElementById("fix-loading-indicator");
const fixWrapperElem = document.getElementById("fix-wrapper");
const fixSectionElem = document.getElementById("fixes-section");
const diffSelectedIndexElem = document.getElementById("diff-counter");
const diffTopElem = document.getElementById("diff-top");
const diffElem = document.getElementById("diff");
const noDiffsElem = document.getElementById("info-no-diffs");
const diffNumElem = document.getElementById("diff-number");
const diffNum2Elem = document.getElementById("diff-number2");
function generateAIFix() {
toggleElement(generateAiFixBtn, "hide");
toggleElement(loadingIndicator, "show");
}
function generateDiffHtml(patch) {
const codeLines = patch.split("\n");
// the first two lines are the file names
codeLines.shift();
codeLines.shift();
const diffHtml = document.createElement("div");
let blockDiv = null;
for (const line of codeLines) {
if (line.startsWith("@@ ")) {
blockDiv = document.createElement("div");
blockDiv.className = "example";
if (blockDiv) {
diffHtml.appendChild(blockDiv);
}
} else {
const lineDiv = document.createElement("div");
lineDiv.className = "example-line";
function showAIFixes(fixes) {
toggleElement(loadingIndicator, 'hide');
toggleElement(fixesSection, 'show');
if (fixes.length > 0) {
diffContainer.innerHTML = fixes.map(fix => `
<div>
<h3>Fix ID: ${'$'}{fix.fixId}</h3>
<pre>${'$'}{fix.unifiedDiffsPerFile['/Users/cata/git/playground/project-with-vulns/lib/insecurity.ts']}</pre>
</div>
`).join('');
} else {
diffContainer.innerHTML = '<p>No fixes available.</p>';
if (line.startsWith("+")) {
lineDiv.classList.add("added");
} else if (line.startsWith("-")) {
lineDiv.classList.add("removed");
}
const lineCode = document.createElement("code");
// if line is empty, we need to fallback to ' '
// to make sure it displays in the diff
lineCode.innerText = line.slice(1, line.length) || " ";
lineDiv.appendChild(lineCode);
blockDiv?.appendChild(lineDiv);
}
}
generateAiFixBtn?.addEventListener('click', generateAIFix);
return diffHtml;
}
function getFilePathFromFix(fix) {
return Object.keys(fix.unifiedDiffsPerFile)[0];
}
function showCurrentDiff(fixes) {
toggleElement(diffTopElem, "show");
toggleElement(diffElem, "show");
toggleElement(noDiffsElem, "hide");
diffNumElem.innerText = fixes.length.toString();
diffNum2Elem.innerText = fixes.length.toString();
diffSelectedIndexElem.innerText = (diffSelectedIndex + 1).toString();
const diffSuggestion = fixes[diffSelectedIndex];
const filePath = getFilePathFromFix(diffSuggestion);
const patch = diffSuggestion.unifiedDiffsPerFile[filePath];
// clear all elements
while (diffElem.firstChild) {
diffElem.removeChild(diffElem.firstChild);
}
diffElem.appendChild(generateDiffHtml(patch));
}
function showAIFixes(fixes) {
toggleElement(fixSectionElem, "show");
toggleElement(loadingIndicator, "hide");
toggleElement(fixWrapperElem, "hide");
showCurrentDiff(fixes);
}
generateAiFixBtn?.addEventListener("click", generateAIFix);
// This function will be called once the response is received from the LS
window.receiveAIFixResponse = function(fixes) {
showAIFixes(fixes);
};
// This function will be called once the response is received from the LS
window.receiveAIFixResponse = function (fixes) {
showAIFixes(fixes);
};
})();
""".trimIndent()
}
Expand Down
37 changes: 15 additions & 22 deletions src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -497,21 +497,31 @@ class LanguageServerWrapper(
return this.getFeatureFlagStatus("snykCodeConsistentIgnores")
}

data class Fix(
@SerializedName("fixId") val fixId: String,
@SerializedName("unifiedDiffsPerFile") val unifiedDiffsPerFile: Map<String, String>
)

@Suppress("UNCHECKED_CAST")
fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): List<Fix> {
println(">> sendCodeFixDiffsCommand: $folderURI, $fileURI, $issueID")
fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): List<Fix> {
if (!ensureLanguageServerInitialized()) return emptyList()

try {
val param = ExecuteCommandParams()
param.command = COMMAND_CODE_FIX_DIFFS
param.arguments = listOf(folderURI, fileURI, issueID)
val result = languageServer.workspaceService.executeCommand(param).get(120, TimeUnit.SECONDS) as List<*>
val diffList : MutableList<Fix> = mutableListOf()
val diffList: MutableList<Fix> = mutableListOf()

result.forEach {
val entry = it as Map<String, *>
val fix = Fix(entry["fixId"]!! as String, entry["unifiedDiffsPerFile"] as Map<String, String>)
diffList.add(fix)
val fixId = entry["fixId"] as? String
val unifiedDiffsPerFile = entry["unifiedDiffsPerFile"] as? Map<String, String>

if (fixId != null && unifiedDiffsPerFile != null) {
val fix = Fix(fixId, unifiedDiffsPerFile)
diffList.add(fix)
}
}
return diffList
} catch (err: Exception) {
Expand All @@ -520,23 +530,6 @@ class LanguageServerWrapper(
}
}

data class Fix(
@SerializedName("fixId") val fixId: String,
@SerializedName("unifiedDiffsPerFile") val unifiedDiffsPerFile: Map<String, String>
)

private fun parseResponse(response: String): List<Fix> {
val gson = Gson()
val processedResponse = preprocessResponse(response)
val type = object : TypeToken<List<Fix>>() {}.type
return gson.fromJson(processedResponse, type)
}

private fun preprocessResponse(response: String): String {
val processed = response.replace("=", ":")
println("Processed response: $processed")
return processed
}

private fun ensureLanguageServerProtocolVersion(project: Project) {
val protocolVersion = initializeResult?.serverInfo?.version
Expand Down
50 changes: 0 additions & 50 deletions src/test/resources/test-fixtures/oss/annotator/go.sum

This file was deleted.

0 comments on commit 7421d80

Please sign in to comment.