From 7421d80207cf6eff4d758beb60c4eb9da5707978 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 4 Sep 2024 13:33:46 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20display=20AI=20suggestions=20=E2=9A=A1?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin/ui/jcef/GenerateAIFixHandler.kt | 17 +- .../toolwindow/panels/JCEFDescriptionPanel.kt | 151 +++++++++++++----- .../snyk/common/lsp/LanguageServerWrapper.kt | 37 ++--- .../test-fixtures/oss/annotator/go.sum | 50 ------ 4 files changed, 132 insertions(+), 123 deletions(-) delete mode 100644 src/test/resources/test-fixtures/oss/annotator/go.sum 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 29dd80b69..0d18244b2 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -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 @@ -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.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) - println("Received responseDiff: $responseDiff") - responseDiff.forEach { fix -> - println("Fix: $fix") - } - + val responseDiff: List = + 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") } 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 38e75ffe7..b50a2f320 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 @@ -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 @@ -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." @@ -161,7 +161,7 @@ class SuggestionDescriptionPanelFromLS( html = html.replace("\${ideStyle}", "") html = html.replace("\${headerEnd}", "") - html = html.replace("\${ideScript}" ,"") + html = html.replace("\${ideScript}", "") val nonce = getNonce() html = html.replace("\${nonce}", nonce) @@ -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 => ` -
-

Fix ID: ${'$'}{fix.fixId}

-
${'$'}{fix.unifiedDiffsPerFile['/Users/cata/git/playground/project-with-vulns/lib/insecurity.ts']}
-
- `).join(''); - } else { - diffContainer.innerHTML = '

No fixes available.

'; + 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() } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index b6f310107..1bbb72f05 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -497,9 +497,13 @@ class LanguageServerWrapper( return this.getFeatureFlagStatus("snykCodeConsistentIgnores") } + data class Fix( + @SerializedName("fixId") val fixId: String, + @SerializedName("unifiedDiffsPerFile") val unifiedDiffsPerFile: Map + ) + @Suppress("UNCHECKED_CAST") - fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): List { - println(">> sendCodeFixDiffsCommand: $folderURI, $fileURI, $issueID") + fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): List { if (!ensureLanguageServerInitialized()) return emptyList() try { @@ -507,11 +511,17 @@ class LanguageServerWrapper( 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 = mutableListOf() + val diffList: MutableList = mutableListOf() + result.forEach { val entry = it as Map - val fix = Fix(entry["fixId"]!! as String, entry["unifiedDiffsPerFile"] as Map) - diffList.add(fix) + val fixId = entry["fixId"] as? String + val unifiedDiffsPerFile = entry["unifiedDiffsPerFile"] as? Map + + if (fixId != null && unifiedDiffsPerFile != null) { + val fix = Fix(fixId, unifiedDiffsPerFile) + diffList.add(fix) + } } return diffList } catch (err: Exception) { @@ -520,23 +530,6 @@ class LanguageServerWrapper( } } - data class Fix( - @SerializedName("fixId") val fixId: String, - @SerializedName("unifiedDiffsPerFile") val unifiedDiffsPerFile: Map - ) - - private fun parseResponse(response: String): List { - val gson = Gson() - val processedResponse = preprocessResponse(response) - val type = object : TypeToken>() {}.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 diff --git a/src/test/resources/test-fixtures/oss/annotator/go.sum b/src/test/resources/test-fixtures/oss/annotator/go.sum deleted file mode 100644 index 084f63e60..000000000 --- a/src/test/resources/test-fixtures/oss/annotator/go.sum +++ /dev/null @@ -1,50 +0,0 @@ -github.com/Unknwon/com v0.0.0-20190321035513-0fed4efef755/go.mod h1:voKvFVpXBJxdIPeqjoJuLK+UVcRlo/JLjeToGxPYu68= -github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966/go.mod h1:SFtfq0zFPsENI7DpE87QM2hcYu5QQ0fRdCgP+P1Hrqo= -github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= -github.com/go-macaron/cache v0.0.0-20151013081102-561735312776/go.mod h1:hHAsZm/oBZVcY+S7qdQL6Vbg5VrXF6RuKGuqsszt3Ok= -github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw= -github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193/go.mod h1:ScEJm9Gk+ez5JJTml5WlBIqavAfuE5nF8e4Gvyz/X+A= -github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= -github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09/go.mod h1:Zas3BtO88pk1cwUfEYlvnl/CRwh0ybDxRWSwRjG8I3w= -github.com/gogs/gogs v0.11.66/go.mod h1:qlbvdn16XTC6q7eR+thjW+OLdN+mi2PBZ8KqVT39T88= -github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a/go.mod h1:TUIZ+29jodWQ8Gk6Pvtg4E09aMsc3C/VLZiVYfUhWQU= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pstember/go-goof/hello v0.0.0-20190715094659-dd899fd4135f/go.mod h1:bsBXgfaZj2NmBzMOxU6GNMwExj6oxUkXV1mY3hJRk/4= -github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/clog.v1 v1.2.0/go.mod h1:L6fgdpdhFgKX4eGuDvt+N6X2GwZE160NRrIHzvaF8ZM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/macaron.v1 v1.3.2/go.mod h1:PrsiawTWAGZs6wFbT5hlr7SQ2Ns9h7cUVtcUu4lQOVo= -gopkg.in/redis.v2 v2.3.2/go.mod h1:4wl9PJ/CqzeHk3LVq1hNLHH8krm3+AXEgut4jVc++LU= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=