From 1ea7c6956141d1fa69d9c9879f086aa9a4e17ac8 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 28 Aug 2024 16:25:40 +0100 Subject: [PATCH 01/28] wip: enable AI Fix --- .../ui/jcef/ThemeBasedStylingGenerator.kt | 3 +- .../toolwindow/panels/JCEFDescriptionPanel.kt | 60 +++++++++++++++---- src/main/kotlin/snyk/common/lsp/Types.kt | 3 +- .../stylesheets/snyk_code_suggestion.scss | 13 ++++ .../stylesheets/snyk_oss_suggestion.scss | 2 +- ...uggestionDescriptionPanelFromLSCodeTest.kt | 2 +- ...SuggestionDescriptionPanelFromLSOSSTest.kt | 2 +- 7 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt index 2089075fd..ec730d442 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt @@ -92,7 +92,8 @@ 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': "#3376CD", // TODO: From Figma. Find the correct JetBrains API to get this color }; for (let [property, value] of Object.entries(properties)) { document.documentElement.style.setProperty(property, value); 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 02bd0bf62..c3fd51bfc 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 @@ -60,7 +60,7 @@ class SuggestionDescriptionPanelFromLS( openFileLoadHandlerGenerator.generate(it) } } - val html = this.getStyledHTML() + val html = this.getCustomCssAndScript() val jbCefBrowserComponent = JCEFUtils.getJBCefBrowserComponentIfSupported(html, loadHandlerGenerators) if (jbCefBrowserComponent == null) { @@ -140,8 +140,9 @@ class SuggestionDescriptionPanelFromLS( return Pair(panel, lastRowToAddSpacer) } - fun getStyledHTML(): String { + fun getCustomCssAndScript(): String { var html = issue.details() + val ideScript = getCustomScript() var ideStyle = "" if (issue.additionalData.getProductType() == ProductType.CODE_SECURITY || issue.additionalData.getProductType() == ProductType.CODE_QUALITY @@ -150,19 +151,22 @@ class SuggestionDescriptionPanelFromLS( } else if (issue.additionalData.getProductType() == ProductType.OSS) { ideStyle = SnykStylesheets.SnykOSSSuggestion } + html = html.replace("\${ideStyle}", "") html = html.replace("\${headerEnd}", "") - html = - html.replace( - "\${ideScript}", - "", - ) +// html = html.replace("\${ideScript}", ideScript) + html = html.replace("\${ideScript}" ,"") +// html = +// html.replace( +// "\${ideScript}", +// "", +// ) val nonce = getNonce() html = html.replace("\${nonce}", nonce) @@ -176,6 +180,36 @@ class SuggestionDescriptionPanelFromLS( .map { allowedChars.random() } .joinToString("") } + + 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); + } + } + + const aiFixWrapperElem = document.getElementById('ai-fix-wrapper'); + const generateAiFix = document.getElementById('generate-ai-fix'); + + // Basic toggle functionality + generateAiFix?.addEventListener('click', () => { + if (aiFixWrapperElem.classList.contains('hidden')) { + toggleElement(aiFixWrapperElem, 'show'); + } else { + toggleElement(aiFixWrapperElem, 'hide'); + } + }); + })(); + """.trimIndent() + } } fun defaultFontLabel( diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 5d1b99310..3553a3305 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -276,8 +276,7 @@ data class ScanIssue( fun canLoadSuggestionPanelFromHTML(): Boolean { return when (this.additionalData.getProductType()) { ProductType.OSS -> true - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> - pluginSettings().isGlobalIgnoresFeatureEnabled && this.additionalData.details != null + ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> this.additionalData.details != null else -> TODO() } diff --git a/src/main/resources/stylesheets/snyk_code_suggestion.scss b/src/main/resources/stylesheets/snyk_code_suggestion.scss index 8f9b6213e..a100fb5b0 100644 --- a/src/main/resources/stylesheets/snyk_code_suggestion.scss +++ b/src/main/resources/stylesheets/snyk_code_suggestion.scss @@ -108,3 +108,16 @@ a, .high-contrast:not(.high-contrast-light) .light-only { display: none; } + +.sn-fix-wrapper { + background-color: var(--container-background-color); // same variable used in snyk_oss_suggestion.scss +} + +button.generate-ai-fix { + background-color: var(--generated-ai-fix-button-background-color); + color: white; +} + +button.generate-ai-fix:hover { + opacity: 0.9; /* Slightly reduces opacity on hover */ +} diff --git a/src/main/resources/stylesheets/snyk_oss_suggestion.scss b/src/main/resources/stylesheets/snyk_oss_suggestion.scss index 87f16d011..217c80c4a 100644 --- a/src/main/resources/stylesheets/snyk_oss_suggestion.scss +++ b/src/main/resources/stylesheets/snyk_oss_suggestion.scss @@ -64,6 +64,6 @@ a, } .vulnerability-overview pre { - background-color: var(--vulnerability-overview-pre-background-color); // VSCode does this styling by default + background-color: var(--container-background-color); // same variable as in snyk_code_suggestion.scss border: 1px solid transparent; } diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt index a717a650b..6cd9d1062 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt @@ -162,7 +162,7 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { every { issue.canLoadSuggestionPanelFromHTML() } returns true cut = SuggestionDescriptionPanelFromLS(snykFile, issue) - val actual = cut.getStyledHTML() + val actual = cut.getCustomCssAndScript() assertFalse(actual.contains("\${ideStyle}")) assertFalse(actual.contains("\${ideScript}")) assertFalse(actual.contains("\${nonce}")) diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt index 418009594..7738adc1b 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -138,7 +138,7 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { every { issue.canLoadSuggestionPanelFromHTML() } returns true cut = SuggestionDescriptionPanelFromLS(snykFile, issue) - val actual = cut.getStyledHTML() + val actual = cut.getCustomCssAndScript() // we don't apply any custom style for oss assertFalse(actual.contains("\${ideStyle}")) From 73b5081a86b9311d98ef6dae3b728f574fa3300f Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Tue, 3 Sep 2024 12:04:01 +0100 Subject: [PATCH 02/28] feat: wip - bridge button action with LS wrapper --- .../plugin/ui/jcef/GenerateAIFixHandler.kt | 49 +++++++++++++++++ .../kotlin/io/snyk/plugin/ui/jcef/Utils.kt | 2 +- .../toolwindow/panels/JCEFDescriptionPanel.kt | 54 ++++++++----------- .../snyk/common/lsp/LanguageServerWrapper.kt | 4 ++ 4 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt new file mode 100644 index 000000000..acc38e49c --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -0,0 +1,49 @@ +import com.intellij.openapi.project.Project +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 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] + + println("Received folderURI: $folderURI, fileURI: $fileURI, issueID: $issueID") + + LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) + 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'); + aiFixButton.addEventListener('click', () => { + const folderURI = "someFolderURI"; // These should be dynamically set + const fileURI = "someFileURI"; + const issueID = "someIssueID"; + window.aiFixQuery(folderURI + ":" + fileURI + ":" + issueID); + }); + })(); + """ + browser.executeJavaScript(script, browser.url, 0) + } + } + } + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt index d5998e20d..00fda7413 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt @@ -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) .setMouseWheelEventEnable(true).build() jbCefBrowser.setOpenLinksInExternalBrowser(true) 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 c3fd51bfc..398023c7e 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,5 +1,6 @@ package io.snyk.plugin.ui.toolwindow.panels +import GenerateAIFixHandler import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.uiDesigner.core.GridLayoutManager @@ -59,6 +60,12 @@ class SuggestionDescriptionPanelFromLS( loadHandlerGenerators += { openFileLoadHandlerGenerator.generate(it) } + + val generateAIFixHandler = GenerateAIFixHandler(snykFile.project) + loadHandlerGenerators += { + generateAIFixHandler.generateAIFixCommand(it) + } + } val html = this.getCustomCssAndScript() val jbCefBrowserComponent = @@ -154,19 +161,7 @@ class SuggestionDescriptionPanelFromLS( html = html.replace("\${ideStyle}", "") html = html.replace("\${headerEnd}", "") -// html = html.replace("\${ideScript}", ideScript) html = html.replace("\${ideScript}" ,"") -// html = -// html.replace( -// "\${ideScript}", -// "", -// ) val nonce = getNonce() html = html.replace("\${nonce}", nonce) @@ -184,29 +179,26 @@ 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 aiFixWrapperElem = document.getElementById('ai-fix-wrapper'); - const generateAiFix = document.getElementById('generate-ai-fix'); + const generateAiFixBtn = document.getElementById('generate-ai-fix'); - // Basic toggle functionality - generateAiFix?.addEventListener('click', () => { - if (aiFixWrapperElem.classList.contains('hidden')) { - toggleElement(aiFixWrapperElem, 'show'); - } else { - toggleElement(aiFixWrapperElem, 'hide'); + function generateAIFix() { + toggleElement(generateAiFixBtn, 'hide'); + toggleElement(document.getElementById('fix-loading-indicator'), 'show'); } - }); + + generateAiFixBtn?.addEventListener('click', generateAIFix); })(); """.trimIndent() } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 9dbe962e7..850ff052f 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -494,6 +494,10 @@ class LanguageServerWrapper( return this.getFeatureFlagStatus("snykCodeConsistentIgnores") } + fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String) { + println("[[sendCodeFixDiffsCommand]]") + } + private fun ensureLanguageServerProtocolVersion(project: Project) { val protocolVersion = initializeResult?.serverInfo?.version pluginSettings().currentLSProtocolVersion = protocolVersion?.toIntOrNull() From 5563b32679ee8ab9432129420de6ed74e2d35009 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Tue, 3 Sep 2024 14:34:04 +0100 Subject: [PATCH 03/28] feat: get Code Diff from LS --- .../plugin/ui/jcef/GenerateAIFixHandler.kt | 9 ++-- .../snyk/common/lsp/LanguageServerWrapper.kt | 17 ++++++- .../snyk/common/lsp/commands/Commands.kt | 1 + .../test-fixtures/oss/annotator/go.sum | 50 +++++++++++++++++++ 4 files changed, 71 insertions(+), 6 deletions(-) create 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 acc38e49c..df5aadf67 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -33,11 +33,12 @@ class GenerateAIFixHandler(private val project: Project) { window.aiFixQuery = function(value) { ${aiFixQuery.inject("value")} }; const aiFixButton = document.getElementById('generate-ai-fix'); + const issueId = aiFixButton.getAttribute('issue-id'); + const folderPath = aiFixButton.getAttribute('folder-path'); + const filePath = aiFixButton.getAttribute('file-path'); + aiFixButton.addEventListener('click', () => { - const folderURI = "someFolderURI"; // These should be dynamically set - const fileURI = "someFileURI"; - const issueID = "someIssueID"; - window.aiFixQuery(folderURI + ":" + fileURI + ":" + issueID); + window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); }); })(); """ diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 850ff052f..3985c72c7 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -46,6 +46,7 @@ import org.eclipse.lsp4j.services.LanguageServer 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_COPY_AUTH_LINK import snyk.common.lsp.commands.COMMAND_EXECUTE_CLI import snyk.common.lsp.commands.COMMAND_GET_ACTIVE_USER @@ -494,8 +495,20 @@ class LanguageServerWrapper( return this.getFeatureFlagStatus("snykCodeConsistentIgnores") } - fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String) { - println("[[sendCodeFixDiffsCommand]]") + fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): Boolean { + if (!ensureLanguageServerInitialized()) return false + + try { + val param = ExecuteCommandParams() + param.command = COMMAND_CODE_FIX_DIFFS + param.arguments = listOf(folderURI, fileURI, issueID) + val result = languageServer.workspaceService.executeCommand(param).get(30, TimeUnit.SECONDS) + println(result) + return result as? Boolean ?: false + } catch (err: Exception) { + println("[[sendCodeFixDiffsCommand]]: $err") + return false + } } private fun ensureLanguageServerProtocolVersion(project: Project) { diff --git a/src/main/kotlin/snyk/common/lsp/commands/Commands.kt b/src/main/kotlin/snyk/common/lsp/commands/Commands.kt index 4cd3032b8..854c789bd 100644 --- a/src/main/kotlin/snyk/common/lsp/commands/Commands.kt +++ b/src/main/kotlin/snyk/common/lsp/commands/Commands.kt @@ -9,3 +9,4 @@ internal const val COMMAND_LOGIN = "snyk.login" 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" diff --git a/src/test/resources/test-fixtures/oss/annotator/go.sum b/src/test/resources/test-fixtures/oss/annotator/go.sum new file mode 100644 index 000000000..084f63e60 --- /dev/null +++ b/src/test/resources/test-fixtures/oss/annotator/go.sum @@ -0,0 +1,50 @@ +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= From b1566535f696cda6e7fcb5bf89c9629ec16230a0 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 4 Sep 2024 09:38:02 +0100 Subject: [PATCH 04/28] debug: test Language Server response --- .../plugin/ui/jcef/GenerateAIFixHandler.kt | 13 ++++++- .../toolwindow/panels/JCEFDescriptionPanel.kt | 24 ++++++++++++ .../snyk/common/lsp/LanguageServerWrapper.kt | 39 +++++++++++++++---- 3 files changed, 68 insertions(+), 8 deletions(-) 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 df5aadf67..29dd80b69 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,4 @@ +import com.google.gson.Gson import com.intellij.openapi.project.Project import com.intellij.ui.jcef.JBCefBrowserBase import com.intellij.ui.jcef.JBCefJSQuery @@ -18,7 +19,16 @@ class GenerateAIFixHandler(private val project: Project) { println("Received folderURI: $folderURI, fileURI: $fileURI, issueID: $issueID") - LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) + val responseDiff: List = LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) + println("Received responseDiff: $responseDiff") + responseDiff.forEach { fix -> + println("Fix: $fix") + } + + val script = """ + window.receiveAIFixResponse(${Gson().toJson(responseDiff)}); + """ + jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) JBCefJSQuery.Response("success") } @@ -38,6 +48,7 @@ class GenerateAIFixHandler(private val project: Project) { const filePath = aiFixButton.getAttribute('file-path'); aiFixButton.addEventListener('click', () => { + console.log('Clicked AI Fix button. Path: ' + folderPath + ':' + filePath + ':' + issueId) window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); }); })(); 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 398023c7e..38e75ffe7 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 @@ -192,13 +192,37 @@ class SuggestionDescriptionPanelFromLS( } 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'); function generateAIFix() { toggleElement(generateAiFixBtn, 'hide'); toggleElement(document.getElementById('fix-loading-indicator'), 'show'); } + 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.

'; + } + } + generateAiFixBtn?.addEventListener('click', generateAIFix); + + // 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 3985c72c7..01ea831e5 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -1,6 +1,7 @@ package snyk.common.lsp import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service @@ -495,22 +496,46 @@ class LanguageServerWrapper( return this.getFeatureFlagStatus("snykCodeConsistentIgnores") } - fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): Boolean { - if (!ensureLanguageServerInitialized()) return false + fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): List { + println(">> sendCodeFixDiffsCommand: $folderURI, $fileURI, $issueID") + 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(30, TimeUnit.SECONDS) - println(result) - return result as? Boolean ?: false + val result = languageServer.workspaceService.executeCommand(param).get(120, TimeUnit.SECONDS) as Map<*, *> +// result["unifiedDiffsPerFile"] + val diff = result["unifiedDiffsPerFile"] + println(diff) + // and parse to an JSON object +// println(result) +// return parseResponse(result.toString()) + return result as List } catch (err: Exception) { - println("[[sendCodeFixDiffsCommand]]: $err") - return false + logger.warn("Error in sendCodeFixDiffsCommand", err) + return emptyList() } } + data class Fix( + val fixId: String, + 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 pluginSettings().currentLSProtocolVersion = protocolVersion?.toIntOrNull() From 575fa56941689630a56c9ea1eed8608aa8126f0b Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 4 Sep 2024 11:00:14 +0200 Subject: [PATCH 05/28] fix: wip parsing of command --- .../snyk/common/lsp/LanguageServerWrapper.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 01ea831e5..b6f310107 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -1,6 +1,7 @@ package snyk.common.lsp import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager @@ -496,6 +497,7 @@ class LanguageServerWrapper( return this.getFeatureFlagStatus("snykCodeConsistentIgnores") } + @Suppress("UNCHECKED_CAST") fun sendCodeFixDiffsCommand(folderURI: String, fileURI: String, issueID: String): List { println(">> sendCodeFixDiffsCommand: $folderURI, $fileURI, $issueID") if (!ensureLanguageServerInitialized()) return emptyList() @@ -504,14 +506,14 @@ class LanguageServerWrapper( 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 Map<*, *> -// result["unifiedDiffsPerFile"] - val diff = result["unifiedDiffsPerFile"] - println(diff) - // and parse to an JSON object -// println(result) -// return parseResponse(result.toString()) - return result as List + val result = languageServer.workspaceService.executeCommand(param).get(120, TimeUnit.SECONDS) as List<*> + 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) + } + return diffList } catch (err: Exception) { logger.warn("Error in sendCodeFixDiffsCommand", err) return emptyList() @@ -519,8 +521,8 @@ class LanguageServerWrapper( } data class Fix( - val fixId: String, - val unifiedDiffsPerFile: Map + @SerializedName("fixId") val fixId: String, + @SerializedName("unifiedDiffsPerFile") val unifiedDiffsPerFile: Map ) private fun parseResponse(response: String): List { From 7421d80207cf6eff4d758beb60c4eb9da5707978 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 4 Sep 2024 13:33:46 +0100 Subject: [PATCH 06/28] =?UTF-8?q?feat:=20display=20AI=20suggestions=20?= =?UTF-8?q?=E2=9A=A1=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= From ff1400b56cac820c5333618595c27ec424400210 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 4 Sep 2024 14:56:37 +0100 Subject: [PATCH 07/28] chore: update `Apply fix` button style --- .../stylesheets/snyk_code_suggestion.scss | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/resources/stylesheets/snyk_code_suggestion.scss b/src/main/resources/stylesheets/snyk_code_suggestion.scss index a100fb5b0..d0cad3c6a 100644 --- a/src/main/resources/stylesheets/snyk_code_suggestion.scss +++ b/src/main/resources/stylesheets/snyk_code_suggestion.scss @@ -109,15 +109,30 @@ a, display: none; } +.example { // AI fix suggestion + background-color: var(--container-background-color); +} + +.sn-ai-fixes { + padding-top: 20px; +} + .sn-fix-wrapper { background-color: var(--container-background-color); // same variable used in snyk_oss_suggestion.scss } -button.generate-ai-fix { +button.generate-ai-fix, button.sn-apply-fix { background-color: var(--generated-ai-fix-button-background-color); color: white; } -button.generate-ai-fix:hover { +button.generate-ai-fix:hover, button.sn-apply-fix:hover { opacity: 0.9; /* Slightly reduces opacity on hover */ } + +button.sn-apply-fix { + border-radius: 3px; + border: 1px solid var(--dark-button-border-default, #4C708C); // TODO: define the variable in the ThemeBasedStylingGenerator class to be theme-based + background: var(--dark-button-default, #365880); // TODO: define the variable in the ThemeBasedStylingGenerator class to be theme-based + padding: 4px 12px; +} From 8a4b2719948def63cf5d7ca019b14210081c2e0a Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 4 Sep 2024 15:31:36 +0100 Subject: [PATCH 08/28] chore: group similar JS logic --- .../toolwindow/panels/JCEFDescriptionPanel.kt | 109 ++++++++---------- 1 file changed, 51 insertions(+), 58 deletions(-) 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 b50a2f320..abff6ab41 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 @@ -179,116 +179,109 @@ class SuggestionDescriptionPanelFromLS( private fun getCustomScript(): String { return """ (function () { - function toggleElement(element, toggle) { + // Utility function to show/hide an element based on a toggle value + function toggleElement(element, action) { 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); - } - } - - let diffSelectedIndex = 0; - - 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"); + element.classList.toggle("hidden", action === "hide"); } + // Generate HTML for the code diff from a patch function generateDiffHtml(patch) { const codeLines = patch.split("\n"); - - // the first two lines are the file names - codeLines.shift(); - codeLines.shift(); + codeLines.splice(0, 2); // Skip the first two lines (file paths) const diffHtml = document.createElement("div"); let blockDiv = null; - for (const line of codeLines) { + codeLines.forEach(line => { if (line.startsWith("@@ ")) { + // Start a new block for a diff hunk blockDiv = document.createElement("div"); blockDiv.className = "example"; - - if (blockDiv) { - diffHtml.appendChild(blockDiv); - } + diffHtml.appendChild(blockDiv); } else { + // Generate a line div and apply the appropriate class based on addition/removal const lineDiv = document.createElement("div"); lineDiv.className = "example-line"; - if (line.startsWith("+")) { lineDiv.classList.add("added"); } else if (line.startsWith("-")) { lineDiv.classList.add("removed"); } + // Create a block for the line content 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) || " "; - + lineCode.innerText = line.slice(1) || " "; // Ensure empty lines display properly lineDiv.appendChild(lineCode); - blockDiv?.appendChild(lineDiv); + + blockDiv?.appendChild(lineDiv); // Append the line to the current block } - } + }); return diffHtml; } + // Extract the file path from the AI fix object function getFilePathFromFix(fix) { return Object.keys(fix.unifiedDiffsPerFile)[0]; } + // Show the diff for the currently selected AI fix function showCurrentDiff(fixes) { toggleElement(diffTopElem, "show"); toggleElement(diffElem, "show"); toggleElement(noDiffsElem, "hide"); - diffNumElem.innerText = fixes.length.toString(); - diffNum2Elem.innerText = fixes.length.toString(); + const totalFixes = fixes.length; + const currentFix = fixes[diffSelectedIndex]; + const filePath = getFilePathFromFix(currentFix); + const patch = currentFix.unifiedDiffsPerFile[filePath]; + // Update diff counters + diffNumElem.innerText = totalFixes.toString(); + diffNum2Elem.innerText = totalFixes.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); - } + // Clear and update the diff container + diffElem.innerHTML = ''; // Clear previous diff diffElem.appendChild(generateDiffHtml(patch)); } + // Show the AI fixes received from the Language Server function showAIFixes(fixes) { toggleElement(fixSectionElem, "show"); - toggleElement(loadingIndicator, "hide"); + toggleElement(fixLoadingIndicatorElem, "hide"); toggleElement(fixWrapperElem, "hide"); showCurrentDiff(fixes); } + // Handle AI fix generation button click + function generateAIFix() { + toggleElement(generateAiFixBtn, "hide"); + toggleElement(fixLoadingIndicatorElem, "show"); + } + + // DOM element references + const generateAiFixBtn = document.getElementById("generate-ai-fix"); + const fixLoadingIndicatorElem = 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"); + + let diffSelectedIndex = 0; + + // Event listener for Generate AI fix button generateAiFixBtn?.addEventListener("click", generateAIFix); - // This function will be called once the response is received from the LS + // This function will be called once the response is received from the Language Server window.receiveAIFixResponse = function (fixes) { showAIFixes(fixes); }; From e03954ce668e84c09f11389603481a3c4963077e Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 4 Sep 2024 15:56:45 +0100 Subject: [PATCH 09/28] fix: responsive UI during AI Fix call with LS async request --- .../plugin/ui/jcef/GenerateAIFixHandler.kt | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) 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 0d18244b2..54891b78b 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -4,6 +4,10 @@ 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 @@ -13,20 +17,32 @@ 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] - JBCefJSQuery.Response("success") - - val responseDiff: List = - 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") + + // Avoids blocking the UI thread + CoroutineScope(Dispatchers.IO).launch { + try { + val responseDiff: List = + LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) + + val script = """ + window.receiveAIFixResponse(${Gson().toJson(responseDiff)}); + """.trimIndent() + + withContext(Dispatchers.Main) { + jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) + JBCefJSQuery.Response("success") + } + } catch (e: Exception) { + e.printStackTrace() + } + } + return@addHandler JBCefJSQuery.Response("success") } return object : CefLoadHandlerAdapter() { From 6c0b5fcaa93c18d55ffe2db2987f990aba48d8c0 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Wed, 4 Sep 2024 16:53:37 +0100 Subject: [PATCH 10/28] feat: wip - apply fix --- .../toolwindow/panels/JCEFDescriptionPanel.kt | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 abff6ab41..e393bcc70 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 @@ -262,8 +262,30 @@ class SuggestionDescriptionPanelFromLS( toggleElement(fixLoadingIndicatorElem, "show"); } + function applyFix() { + console.log('Applying fix', fixes); + if (!fixes.length) return; + + const currentFix = fixes[diffSelectedIndex]; + const filePath = getFilePathFromFix(currentFix); + const patch = currentFix.unifiedDiffsPerFile[filePath]; + + // Following VSCode logic, the steps are: + // 1. Read the current file content. + // 2. Apply a patch to that content. + // 3. Edit the file in the workspace. + // 4. Highlight the added code. + // 5. Setup close or save events. + console.log('Applying fix', patch); + } + // DOM element references const generateAiFixBtn = document.getElementById("generate-ai-fix"); + const applyFixBtn = document.getElementById('apply-fix') + const retryGenerateFixBtn = document.getElementById('retry-generate-fix') + + console.log('applyFixBtn', applyFixBtn) + const fixLoadingIndicatorElem = document.getElementById("fix-loading-indicator"); const fixWrapperElem = document.getElementById("fix-wrapper"); const fixSectionElem = document.getElementById("fixes-section"); @@ -277,12 +299,15 @@ class SuggestionDescriptionPanelFromLS( const diffNum2Elem = document.getElementById("diff-number2"); let diffSelectedIndex = 0; + let fixes = []; // Event listener for Generate AI fix button generateAiFixBtn?.addEventListener("click", generateAIFix); + applyFixBtn?.addEventListener('click', applyFix); // This function will be called once the response is received from the Language Server - window.receiveAIFixResponse = function (fixes) { + window.receiveAIFixResponse = function (fixesResponse) { + fixes = [...fixesResponse]; showAIFixes(fixes); }; })(); From 281670ab1d4f02867cb0b4b018bfa64b800ecfbf Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Thu, 5 Sep 2024 09:16:35 +0200 Subject: [PATCH 11/28] fix: added support for navigating between AI Fixes --- .../toolwindow/panels/JCEFDescriptionPanel.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 e393bcc70..fc5b355b3 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 @@ -185,6 +185,20 @@ class SuggestionDescriptionPanelFromLS( element.classList.toggle("hidden", action === "hide"); } + function nextDiff() { + console.log('nextDiff') + if (!fixes || diffSelectedIndex >= fixes.length - 1) return; + ++diffSelectedIndex; + showCurrentDiff(fixes); + } + + function previousDiff() { + console.log('previousDiff') + if (!fixes || diffSelectedIndex <= 0) return; + --diffSelectedIndex; + showCurrentDiff(fixes) + } + // Generate HTML for the code diff from a patch function generateDiffHtml(patch) { const codeLines = patch.split("\n"); @@ -290,6 +304,8 @@ class SuggestionDescriptionPanelFromLS( const fixWrapperElem = document.getElementById("fix-wrapper"); const fixSectionElem = document.getElementById("fixes-section"); + const nextDiffElem = document.getElementById('next-diff'); + const previousDiffElem = document.getElementById('previous-diff'); const diffSelectedIndexElem = document.getElementById("diff-counter"); const diffTopElem = document.getElementById("diff-top"); const diffElem = document.getElementById("diff"); @@ -305,6 +321,9 @@ class SuggestionDescriptionPanelFromLS( generateAiFixBtn?.addEventListener("click", generateAIFix); applyFixBtn?.addEventListener('click', applyFix); + nextDiffElem?.addEventListener("click", nextDiff); + previousDiffElem?.addEventListener("click", previousDiff); + // This function will be called once the response is received from the Language Server window.receiveAIFixResponse = function (fixesResponse) { fixes = [...fixesResponse]; From c987b116520e78d5594583cc25168252f6c70529 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Thu, 5 Sep 2024 14:47:10 +0200 Subject: [PATCH 12/28] feat: show section for error, if ai fix reqest fails to generate fixes --- .../ui/toolwindow/panels/JCEFDescriptionPanel.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 fc5b355b3..b1159639e 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 @@ -276,6 +276,13 @@ class SuggestionDescriptionPanelFromLS( toggleElement(fixLoadingIndicatorElem, "show"); } + function showGenerateAIFixError() { + toggleElement(fixLoadingIndicatorElem, "hide"); + toggleElement(fixWrapperElem, "hide"); + toggleElement(fixSectionElem, "hide"); + toggleElement(fixErrorSectionElem, "show"); + } + function applyFix() { console.log('Applying fix', fixes); if (!fixes.length) return; @@ -303,6 +310,7 @@ class SuggestionDescriptionPanelFromLS( const fixLoadingIndicatorElem = document.getElementById("fix-loading-indicator"); const fixWrapperElem = document.getElementById("fix-wrapper"); const fixSectionElem = document.getElementById("fixes-section"); + const fixErrorSectionElem = document.getElementById("fixes-error-section"); const nextDiffElem = document.getElementById('next-diff'); const previousDiffElem = document.getElementById('previous-diff'); @@ -327,6 +335,10 @@ class SuggestionDescriptionPanelFromLS( // This function will be called once the response is received from the Language Server window.receiveAIFixResponse = function (fixesResponse) { fixes = [...fixesResponse]; + if (!fixes.length) { + showGenerateAIFixError(); + return; + } showAIFixes(fixes); }; })(); From ddea6a74a9431fb87edb23b909dcca05e378698a Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Thu, 5 Sep 2024 15:00:22 +0200 Subject: [PATCH 13/28] fix: add the same spacing for error section as for show fixes section --- src/main/resources/stylesheets/snyk_code_suggestion.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/stylesheets/snyk_code_suggestion.scss b/src/main/resources/stylesheets/snyk_code_suggestion.scss index d0cad3c6a..f941abe64 100644 --- a/src/main/resources/stylesheets/snyk_code_suggestion.scss +++ b/src/main/resources/stylesheets/snyk_code_suggestion.scss @@ -117,6 +117,10 @@ a, padding-top: 20px; } +.sn-ai-fix-error { + padding-top: 20px; +} + .sn-fix-wrapper { background-color: var(--container-background-color); // same variable used in snyk_oss_suggestion.scss } From dbaacd1f0d5f755ce8d4b73479fca7fa1d24e485 Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Thu, 5 Sep 2024 16:22:30 +0100 Subject: [PATCH 14/28] chore: wip - apply fix --- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 136 ++++++++++++++++++ .../toolwindow/panels/JCEFDescriptionPanel.kt | 18 +++ 2 files changed, 154 insertions(+) create mode 100644 src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt new file mode 100644 index 000000000..f301e099c --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -0,0 +1,136 @@ +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 + +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 + + // 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) { + e.printStackTrace() + } + } + 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) + } + } + } + } + + // Applies a patch to the file content and saves it + private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Boolean { + val virtualFile = findVirtualFile(filePath) + + if (virtualFile == null) { + println("[applyPatchAndSave] Virtual file not found for path: $filePath") + return false + } + + // Use runReadAction to read the file content + val fileContent = ApplicationManager.getApplication().runReadAction { + val document = FileDocumentManager.getInstance().getDocument(virtualFile) + document?.text + } + + // If fileContent is null, log the error and return + if (fileContent == null) { + println("[applyPatchAndSave] Document not found or is null for virtual file: $filePath") + return false + } + + // Apply the patch to the content outside any read or write actions + val patchedContent = applyPatch(fileContent, patch) + + // If the patch fails to apply, log and return + if (patchedContent == null) { + println("[applyPatchAndSave] Failed to apply patch.") + return false + } + + // Now apply the patch inside a WriteCommandAction + WriteCommandAction.runWriteCommandAction(project) { + val document = FileDocumentManager.getInstance().getDocument(virtualFile) + if (document != null) { + document.setText(patchedContent) + FileDocumentManager.getInstance().saveDocument(document) + println("[applyPatchAndSave] Patch applied successfully!") + } + } + + return true + } + + private fun applyPatch(fileContent: String, patch: String): String? { + println("[applyPatch] Applying patch: $patch") + + // Simple patch application (find & replace logic) + val patchLines = patch.lines() + val patchedContent = StringBuilder(fileContent) + + for (line in patchLines) { + if (line.startsWith("-")) { + // Remove the line that starts with '-' + val oldLine = line.substring(1).trim() + val startIndex = patchedContent.indexOf(oldLine) + if (startIndex != -1) { + patchedContent.delete(startIndex, startIndex + oldLine.length) + } + } else if (line.startsWith("+")) { + // Add the line that starts with '+' + val newLine = line.substring(1).trim() + patchedContent.append("\n").append(newLine) + } + } + + return patchedContent.toString() + } + + private fun findVirtualFile(filePath: String): VirtualFile? { + return LocalFileSystem.getInstance().findFileByPath(filePath) + } +} 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 b1159639e..8cc6b8827 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 @@ -10,6 +10,7 @@ import io.snyk.plugin.ui.DescriptionHeaderPanel import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.baseGridConstraintsAnchorWest import io.snyk.plugin.ui.descriptionHeaderPanel +import io.snyk.plugin.ui.jcef.ApplyFixHandler import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.jcef.LoadHandlerGenerator import io.snyk.plugin.ui.jcef.OpenFileLoadHandlerGenerator @@ -66,6 +67,10 @@ class SuggestionDescriptionPanelFromLS( generateAIFixHandler.generateAIFixCommand(it) } + val applyFixHandler = ApplyFixHandler(snykFile.project) + loadHandlerGenerators += { + applyFixHandler.generateApplyFixCommand(it) + } } val html = this.getCustomCssAndScript() val jbCefBrowserComponent = @@ -291,6 +296,8 @@ class SuggestionDescriptionPanelFromLS( const filePath = getFilePathFromFix(currentFix); const patch = currentFix.unifiedDiffsPerFile[filePath]; + window.applyFixQuery(filePath + ':' + patch); + // Following VSCode logic, the steps are: // 1. Read the current file content. // 2. Apply a patch to that content. @@ -341,6 +348,17 @@ class SuggestionDescriptionPanelFromLS( } showAIFixes(fixes); }; + + window.receiveApplyFixResponse = function (success) { + console.log('[[receiveApplyFixResponse]]', success); + if (success) { + applyFixBtn.disabled = true; + console.log('Fix applied', success); + document.getElementById('apply-fix').disabled = true; + } else { + console.error('Failed to apply fix', success); + } + }; })(); """.trimIndent() } From d0937bad788a7da5fa04252cabc1dd127499cd6c Mon Sep 17 00:00:00 2001 From: Catalina Oyaneder Date: Fri, 6 Sep 2024 10:01:28 +0100 Subject: [PATCH 15/28] chore: wip add test to handle patches --- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 153 ++++++++++++---- .../plugin/ui/jcef/ApplyFixHandlerTest.kt | 163 ++++++++++++++++++ 2 files changed, 279 insertions(+), 37 deletions(-) create mode 100644 src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index f301e099c..80b883817 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -16,6 +16,26 @@ 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 +) + +data class Hunk( + val startLineOriginal: Int, + val numLinesOriginal: Int, + val startLineFixed: Int, + val numLinesFixed: Int, + val changes: List +) + +sealed class Change { + 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 { @@ -26,6 +46,10 @@ class ApplyFixHandler(private val project: Project) { 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") + + // Avoid blocking the UI thread CoroutineScope(Dispatchers.IO).launch { try { @@ -62,72 +86,127 @@ class ApplyFixHandler(private val project: Project) { } } - // Applies a patch to the file content and saves it private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Boolean { - val virtualFile = findVirtualFile(filePath) - - if (virtualFile == null) { + val virtualFile = findVirtualFile(filePath) ?: run { println("[applyPatchAndSave] Virtual file not found for path: $filePath") return false } - // Use runReadAction to read the file content + println("[applyPatchAndSave] Found virtual file: $virtualFile") + val fileContent = ApplicationManager.getApplication().runReadAction { val document = FileDocumentManager.getInstance().getDocument(virtualFile) document?.text - } - - // If fileContent is null, log the error and return - if (fileContent == null) { + } ?: run { println("[applyPatchAndSave] Document not found or is null for virtual file: $filePath") return false } - // Apply the patch to the content outside any read or write actions - val patchedContent = applyPatch(fileContent, patch) + println("[applyPatchAndSave] Initial file content: $fileContent") - // If the patch fails to apply, log and return - if (patchedContent == null) { - println("[applyPatchAndSave] Failed to apply patch.") - return false - } + val diffPatch = parseDiff(patch) + val patchedContent = applyPatch(fileContent, diffPatch) - // Now apply the patch inside a WriteCommandAction + println("[applyPatchAndSave] Patched content that will be written: $patchedContent") + + // Apply the patch inside a WriteCommandAction WriteCommandAction.runWriteCommandAction(project) { val document = FileDocumentManager.getInstance().getDocument(virtualFile) - if (document != null) { - document.setText(patchedContent) - FileDocumentManager.getInstance().saveDocument(document) + document?.let { + it.setText(patchedContent) + + println("[applyPatchAndSave] Content after applying patch: ${it.text}") + + FileDocumentManager.getInstance().saveDocument(it) println("[applyPatchAndSave] Patch applied successfully!") + } ?: run { + println("[applyPatchAndSave] Failed to find document for saving patched content.") } } return true } - private fun applyPatch(fileContent: String, patch: String): String? { - println("[applyPatch] Applying patch: $patch") + fun applyPatch(fileContent: String, diffPatch: DiffPatch): String { + val lines = fileContent.lines().toMutableList() - // Simple patch application (find & replace logic) - val patchLines = patch.lines() - val patchedContent = StringBuilder(fileContent) + for (hunk in diffPatch.hunks) { + var originalLineIndex = hunk.startLineOriginal - 1 // Convert to 0-based index - for (line in patchLines) { - if (line.startsWith("-")) { - // Remove the line that starts with '-' - val oldLine = line.substring(1).trim() - val startIndex = patchedContent.indexOf(oldLine) - if (startIndex != -1) { - patchedContent.delete(startIndex, startIndex + oldLine.length) + 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 + } } - } else if (line.startsWith("+")) { - // Add the line that starts with '+' - val newLine = line.substring(1).trim() - patchedContent.append("\n").append(newLine) } } + 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() + var currentHunk: Hunk? = null + val changes = mutableListOf() + + 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 patchedContent.toString() + return DiffPatch( + originalFile = originalFile, + fixedFile = fixedFile, + hunks = hunks + ) } private fun findVirtualFile(filePath: String): VirtualFile? { diff --git a/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt b/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt new file mode 100644 index 000000000..76725efc1 --- /dev/null +++ b/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt @@ -0,0 +1,163 @@ +package io.snyk.plugin.ui.jcef + +import org.junit.Test +import junit.framework.TestCase.assertEquals + +class DiffPatchTest { + private val originalFileContent = """ + /* + * Copyright (c) 2014-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + + import path = require('path') + import { type Request, type Response } from 'express' + + import challengeUtils = require('../lib/challengeUtils') + const challenges = require('../data/datacache').challenges + + module.exports = function servePremiumContent () { + return (req: Request, res: Response) => { + challengeUtils.solveIf(challenges.premiumPaywallChallenge, () => { return true }) + res.sendFile(path.resolve('frontend/dist/frontend/assets/private/JuiceShop_Wallpaper_1920x1080_VR.jpg')) + } + } + """.trimIndent() + + private val responseDiff = """ + --- /Users/cata/git/playground/project-with-vulns + +++ /Users/cata/git/playground/project-with-vulns-fixed + @@ -4,9 +4,14 @@ + */ + + import path = require('path') + +import rateLimit = require('express-rate-limit') + import { type Request, type Response } from 'express' + + import challengeUtils = require('../lib/challengeUtils') + +const apiLimiter = rateLimit({ + + windowMs: 15 * 60 * 1000, // 15 minutes + + max: 100, // limit each IP to 100 requests per windowMs + +}) + const challenges = require('../data/datacache').challenges + + module.exports = function servePremiumContent () { + """.trimIndent() + + @Test + fun `test applying patch`() { + val diffPatch = parseDiff(responseDiff) + val patchedContent = applyPatch(originalFileContent, diffPatch) + + val expectedPatchedContent = """ + /* + * Copyright (c) 2014-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. + * SPDX-License-Identifier: MIT + */ + + import path = require('path') + import rateLimit = require('express-rate-limit') + import { type Request, type Response } from 'express' + + import challengeUtils = require('../lib/challengeUtils') + const apiLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + }) + const challenges = require('../data/datacache').challenges + + module.exports = function servePremiumContent () { + return (req: Request, res: Response) => { + challengeUtils.solveIf(challenges.premiumPaywallChallenge, () => { return true }) + res.sendFile(path.resolve('frontend/dist/frontend/assets/private/JuiceShop_Wallpaper_1920x1080_VR.jpg')) + } + } + """.trimIndent() + + assertEquals(expectedPatchedContent, patchedContent) + } + + private 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") + } + + private 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() + var currentHunk: Hunk? = null + val changes = mutableListOf() + + 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 + ) + } +} From 8a387b6e49afd7b51f28cafbef89db2dbb10c5d6 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Fri, 6 Sep 2024 15:32:45 +0200 Subject: [PATCH 16/28] fix: fix flow for regenerating ai fixes after error --- .../io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt | 6 +++++- .../ui/toolwindow/panels/JCEFDescriptionPanel.kt | 13 +++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) 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 54891b78b..3ffe79644 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -56,12 +56,16 @@ class GenerateAIFixHandler(private val project: Project) { 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', () => { - console.log('Clicked AI Fix button. Path: ' + folderPath + ':' + filePath + ':' + issueId) + window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); + }); + + retryFixButton.addEventListener('click', () => { window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); }); })(); 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 8cc6b8827..b82b05035 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 @@ -191,14 +191,12 @@ class SuggestionDescriptionPanelFromLS( } function nextDiff() { - console.log('nextDiff') if (!fixes || diffSelectedIndex >= fixes.length - 1) return; ++diffSelectedIndex; showCurrentDiff(fixes); } function previousDiff() { - console.log('previousDiff') if (!fixes || diffSelectedIndex <= 0) return; --diffSelectedIndex; showCurrentDiff(fixes) @@ -281,6 +279,14 @@ class SuggestionDescriptionPanelFromLS( toggleElement(fixLoadingIndicatorElem, "show"); } + // Handle AI fix re-generation button click + function reGenerateAIFix() { + toggleElement(fixErrorSectionElem, "hide"); + toggleElement(fixWrapperElem, "show"); + + generateAIFix() + } + function showGenerateAIFixError() { toggleElement(fixLoadingIndicatorElem, "hide"); toggleElement(fixWrapperElem, "hide"); @@ -312,8 +318,6 @@ class SuggestionDescriptionPanelFromLS( const applyFixBtn = document.getElementById('apply-fix') const retryGenerateFixBtn = document.getElementById('retry-generate-fix') - console.log('applyFixBtn', applyFixBtn) - const fixLoadingIndicatorElem = document.getElementById("fix-loading-indicator"); const fixWrapperElem = document.getElementById("fix-wrapper"); const fixSectionElem = document.getElementById("fixes-section"); @@ -335,6 +339,7 @@ class SuggestionDescriptionPanelFromLS( // Event listener for Generate AI fix button generateAiFixBtn?.addEventListener("click", generateAIFix); applyFixBtn?.addEventListener('click', applyFix); + retryGenerateFixBtn?.addEventListener('click', reGenerateAIFix); nextDiffElem?.addEventListener("click", nextDiff); previousDiffElem?.addEventListener("click", previousDiff); From e0fc1d22133e1825ceeea24aef1c169d04adb067 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Wed, 11 Sep 2024 15:03:52 +0200 Subject: [PATCH 17/28] tidy: replace hardcoded colors with JBUI theme colors --- .../io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt | 9 +++++++-- src/main/resources/stylesheets/snyk_code_suggestion.scss | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt index ec730d442..a253ad914 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt @@ -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 @@ -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 @@ -93,7 +95,10 @@ class ThemeBasedStylingGenerator { '--editor-color': "$editorColor", '--label-color': "'$labelColor'", '--container-background-color': "${toCssHex(codeTagBgColor)}", - '--generated-ai-fix-button-background-color': "#3376CD", // TODO: From Figma. Find the correct JetBrains API to get this color + '--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); diff --git a/src/main/resources/stylesheets/snyk_code_suggestion.scss b/src/main/resources/stylesheets/snyk_code_suggestion.scss index f941abe64..ba2c6c3ef 100644 --- a/src/main/resources/stylesheets/snyk_code_suggestion.scss +++ b/src/main/resources/stylesheets/snyk_code_suggestion.scss @@ -136,7 +136,7 @@ button.generate-ai-fix:hover, button.sn-apply-fix:hover { button.sn-apply-fix { border-radius: 3px; - border: 1px solid var(--dark-button-border-default, #4C708C); // TODO: define the variable in the ThemeBasedStylingGenerator class to be theme-based - background: var(--dark-button-default, #365880); // TODO: define the variable in the ThemeBasedStylingGenerator class to be theme-based + border: 1px solid var(--dark-button-border-default); + background: var(--dark-button-default); padding: 4px 12px; } From adaca2aaab87f089bff6835da041bfffa5df4ee7 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Thu, 12 Sep 2024 10:03:33 +0200 Subject: [PATCH 18/28] tidy: remove println's and disable devtools --- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 21 +++++-------------- .../kotlin/io/snyk/plugin/ui/jcef/Utils.kt | 2 +- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index 80b883817..3aaf88b5d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.withContext import org.cef.browser.CefBrowser import org.cef.browser.CefFrame import org.cef.handler.CefLoadHandlerAdapter +import com.intellij.openapi.diagnostic.Logger data class DiffPatch( val originalFile: String, @@ -37,6 +38,7 @@ sealed class Change { } class ApplyFixHandler(private val project: Project) { + private val logger = Logger.getInstance(this::class.java) fun generateApplyFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { val applyFixQuery = JBCefJSQuery.create(jbCefBrowser) @@ -46,10 +48,6 @@ class ApplyFixHandler(private val project: Project) { 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") - - // Avoid blocking the UI thread CoroutineScope(Dispatchers.IO).launch { try { @@ -88,39 +86,30 @@ class ApplyFixHandler(private val project: Project) { private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Boolean { val virtualFile = findVirtualFile(filePath) ?: run { - println("[applyPatchAndSave] Virtual file not found for path: $filePath") + logger.debug("[applyPatchAndSave] Virtual file not found for path: $filePath") return false } - println("[applyPatchAndSave] Found virtual file: $virtualFile") - val fileContent = ApplicationManager.getApplication().runReadAction { val document = FileDocumentManager.getInstance().getDocument(virtualFile) document?.text } ?: run { - println("[applyPatchAndSave] Document not found or is null for virtual file: $filePath") + logger.debug("[applyPatchAndSave] Document not found or is null for virtual file: $filePath") return false } - println("[applyPatchAndSave] Initial file content: $fileContent") - val diffPatch = parseDiff(patch) val patchedContent = applyPatch(fileContent, diffPatch) - println("[applyPatchAndSave] Patched content that will be written: $patchedContent") - // 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}") - FileDocumentManager.getInstance().saveDocument(it) - println("[applyPatchAndSave] Patch applied successfully!") } ?: run { - println("[applyPatchAndSave] Failed to find document for saving patched content.") + logger.debug("[applyPatchAndSave] Failed to find document for saving patched content.\"") } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt index 00fda7413..d5998e20d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt @@ -19,7 +19,7 @@ object JCEFUtils { val cefClient = JBCefApp.getInstance().createClient() cefClient.setProperty("JS_QUERY_POOL_SIZE", 1) val jbCefBrowser = - JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(true) + JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(false) .setMouseWheelEventEnable(true).build() jbCefBrowser.setOpenLinksInExternalBrowser(true) From 097875a73bf89b990433531fa5686d027467caeb Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Fri, 13 Sep 2024 16:39:36 +0200 Subject: [PATCH 19/28] chore: updating logging and error handling, changing threading to runAsync --- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 72 +++++++++++-------- .../plugin/ui/jcef/GenerateAIFixHandler.kt | 17 ++--- .../ui/jcef/ThemeBasedStylingGenerator.kt | 5 +- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index 3aaf88b5d..f32a9250d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -2,20 +2,17 @@ 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.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 -import com.intellij.openapi.diagnostic.Logger +import org.jetbrains.concurrency.runAsync +import io.snyk.plugin.toVirtualFile + data class DiffPatch( val originalFile: String, @@ -38,30 +35,36 @@ sealed class Change { } 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) + fun generateApplyFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { val applyFixQuery = JBCefJSQuery.create(jbCefBrowser) + //temporary logs + logger.info("[generateApplyFixCommand] calling applyPatchAndSave") + 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 // Avoid blocking the UI thread - CoroutineScope(Dispatchers.IO).launch { + runAsync { try { val success = applyPatchAndSave(project, filePath, patch) + val script = """ window.receiveApplyFixResponse($success); """.trimIndent() - withContext(Dispatchers.Main) { - jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) - } + jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) } catch (e: Exception) { - e.printStackTrace() + log("Error applying fix: ${e.message}") } } return@addHandler JBCefJSQuery.Response("success") @@ -85,35 +88,40 @@ class ApplyFixHandler(private val project: Project) { } private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Boolean { - val virtualFile = findVirtualFile(filePath) ?: run { - logger.debug("[applyPatchAndSave] Virtual file not found for path: $filePath") - return false - } + val virtualFile = filePath.toVirtualFile() val fileContent = ApplicationManager.getApplication().runReadAction { val document = FileDocumentManager.getInstance().getDocument(virtualFile) document?.text } ?: run { - logger.debug("[applyPatchAndSave] Document not found or is null for virtual file: $filePath") + log("[applyPatchAndSave] Document not found or is null for virtual file: $filePath") return false } val diffPatch = parseDiff(patch) val patchedContent = applyPatch(fileContent, diffPatch) + var result = false // Apply the patch inside a WriteCommandAction - WriteCommandAction.runWriteCommandAction(project) { - val document = FileDocumentManager.getInstance().getDocument(virtualFile) - document?.let { - it.setText(patchedContent) - - FileDocumentManager.getInstance().saveDocument(it) - } ?: run { - logger.debug("[applyPatchAndSave] Failed to find document for saving patched content.\"") + result = try { + WriteCommandAction.runWriteCommandAction(project) { + val document = FileDocumentManager.getInstance().getDocument(virtualFile) + if (document != null) { + document.setText(patchedContent) + + //temporary logs + logger.info("[applyPatchAndSave] Content after applying patch:${document.text}") + } else { + log("[applyPatchAndSave] Failed to find document for saving patched content.") + } } + true // Success + } catch (e: Exception) { + log("[applyPatchAndSave] Error applying patch: ${e.message}") + false // Failure } - return true + return result } fun applyPatch(fileContent: String, diffPatch: DiffPatch): String { @@ -128,11 +136,13 @@ class ApplyFixHandler(private val project: Project) { 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 } @@ -176,10 +186,12 @@ class ApplyFixHandler(private val project: Project) { 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())) @@ -198,7 +210,11 @@ class ApplyFixHandler(private val project: Project) { ) } - private fun findVirtualFile(filePath: String): VirtualFile? { - return LocalFileSystem.getInstance().findFileByPath(filePath) + private fun log(logMessage: String) { + when { + enableDebug -> logger.debug(logMessage) + enableTrace -> logger.trace(logMessage) + else -> logger.error(logMessage) + } } } 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 3ffe79644..a21a14818 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -4,13 +4,10 @@ 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 org.jetbrains.concurrency.runAsync import snyk.common.lsp.LanguageServerWrapper class GenerateAIFixHandler(private val project: Project) { @@ -24,20 +21,20 @@ class GenerateAIFixHandler(private val project: Project) { val fileURI = params[1] val issueID = params[2] - // Avoids blocking the UI thread - CoroutineScope(Dispatchers.IO).launch { + + runAsync { try { val responseDiff: List = LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) + //What happens when sendCodeFixDiffsCommand fails or is empty? + val script = """ window.receiveAIFixResponse(${Gson().toJson(responseDiff)}); """.trimIndent() - withContext(Dispatchers.Main) { - jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) - JBCefJSQuery.Response("success") - } + jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) + JBCefJSQuery.Response("success") } catch (e: Exception) { e.printStackTrace() } diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt index a253ad914..661941ace 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt @@ -62,10 +62,11 @@ class ThemeBasedStylingGenerator { val baseColor = UIUtil.getTextFieldBackground() val (addedColor, removedColor) = getCodeDiffColors(baseColor, isHighContrast) - val textColor = toCssHex(JBUI.CurrentTheme.Label.foreground()) - val linkColor = toCssHex(JBUI.CurrentTheme.Link.Foreground.ENABLED) val dataFlowColor = toCssHex(baseColor) val editorColor = toCssHex(baseColor) + + val textColor = toCssHex(JBUI.CurrentTheme.Label.foreground()) + val linkColor = toCssHex(JBUI.CurrentTheme.Link.Foreground.ENABLED) val borderColor = toCssHex(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()) val labelColor = toCssHex(JBUI.CurrentTheme.Label.foreground()) From 16ae0a163f60de7f26c40110448e9bbbfb6d9e7b Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 16 Sep 2024 15:18:57 +0200 Subject: [PATCH 20/28] fix: solved a problem with applying patch --- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 13 ++++++++----- .../ui/toolwindow/panels/JCEFDescriptionPanel.kt | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index f32a9250d..0122601ab 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -48,7 +48,7 @@ class ApplyFixHandler(private val project: Project) { logger.info("[generateApplyFixCommand] calling applyPatchAndSave") applyFixQuery.addHandler { value -> - val params = value.split(":") + 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 @@ -63,6 +63,7 @@ class ApplyFixHandler(private val project: Project) { """.trimIndent() jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) + JBCefJSQuery.Response("success") } catch (e: Exception) { log("Error applying fix: ${e.message}") } @@ -100,22 +101,24 @@ class ApplyFixHandler(private val project: Project) { val diffPatch = parseDiff(patch) val patchedContent = applyPatch(fileContent, diffPatch) - var result = false // Apply the patch inside a WriteCommandAction - result = try { + val result = try { + var appliedPatchSuccessfully = true WriteCommandAction.runWriteCommandAction(project) { val document = FileDocumentManager.getInstance().getDocument(virtualFile) - if (document != null) { + if (document != null && fileContent != patchedContent) { document.setText(patchedContent) //temporary logs logger.info("[applyPatchAndSave] Content after applying patch:${document.text}") + appliedPatchSuccessfully = true } else { log("[applyPatchAndSave] Failed to find document for saving patched content.") + appliedPatchSuccessfully = false } } - true // Success + appliedPatchSuccessfully } catch (e: Exception) { log("[applyPatchAndSave] Error applying patch: ${e.message}") false // Failure 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 b82b05035..667d77f48 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 @@ -302,7 +302,8 @@ class SuggestionDescriptionPanelFromLS( const filePath = getFilePathFromFix(currentFix); const patch = currentFix.unifiedDiffsPerFile[filePath]; - window.applyFixQuery(filePath + ':' + patch); + + window.applyFixQuery(filePath + '|@' + patch); // Following VSCode logic, the steps are: // 1. Read the current file content. From 4387f5d9f56b32b175b7de35c856a4dcb8c8cbff Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 16 Sep 2024 15:31:23 +0200 Subject: [PATCH 21/28] chore: replace colors with colors from JBUI --- .../ui/jcef/ThemeBasedStylingGenerator.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt index 661941ace..f50caa3f9 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt @@ -3,10 +3,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.JBColor import com.intellij.ui.jcef.JBCefBrowserBase import com.intellij.util.ui.JBUI -import com.intellij.util.ui.PlatformColors +import com.intellij.ui.JBColor import com.intellij.util.ui.UIUtil import org.cef.browser.CefBrowser import org.cef.browser.CefFrame @@ -59,21 +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 + 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.Label.foreground()) + 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 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 val themeScript = """ (function(){ @@ -96,9 +95,9 @@ class ThemeBasedStylingGenerator { '--editor-color': "$editorColor", '--label-color': "'$labelColor'", '--container-background-color': "${toCssHex(codeTagBgColor)}", - '--generated-ai-fix-button-background-color': "$linkColor", + '--generated-ai-fix-button-background-color': "$buttonColor", '--dark-button-border-default': "$borderColor", - '--dark-button-default': "$linkColor", + '--dark-button-default': "$buttonColor", }; for (let [property, value] of Object.entries(properties)) { From cc02eb7649b52ce0940edadc17f4af3173a321cf Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 16 Sep 2024 15:56:59 +0200 Subject: [PATCH 22/28] tidy: cleanup code, remove temporary logging --- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 16 ++++------------ .../snyk/plugin/ui/jcef/GenerateAIFixHandler.kt | 16 +++++----------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index 0122601ab..5221cb361 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -44,9 +44,6 @@ class ApplyFixHandler(private val project: Project) { fun generateApplyFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { val applyFixQuery = JBCefJSQuery.create(jbCefBrowser) - //temporary logs - logger.info("[generateApplyFixCommand] calling applyPatchAndSave") - applyFixQuery.addHandler { value -> val params = value.split("|@", limit = 2) val filePath = params[0] // Path to the file that needs to be patched @@ -54,19 +51,14 @@ class ApplyFixHandler(private val project: Project) { // Avoid blocking the UI thread runAsync { - try { - val success = applyPatchAndSave(project, filePath, patch) - + val success = applyPatchAndSave(project, filePath, patch) - val script = """ + val script = """ window.receiveApplyFixResponse($success); """.trimIndent() - jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) - JBCefJSQuery.Response("success") - } catch (e: Exception) { - log("Error applying fix: ${e.message}") - } + jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) + JBCefJSQuery.Response("success") } return@addHandler JBCefJSQuery.Response("success") } 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 a21a14818..93e08c3af 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -23,21 +23,15 @@ class GenerateAIFixHandler(private val project: Project) { runAsync { - try { - val responseDiff: List = - LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) + val responseDiff: List = + LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) - //What happens when sendCodeFixDiffsCommand fails or is empty? - - val script = """ + val script = """ window.receiveAIFixResponse(${Gson().toJson(responseDiff)}); """.trimIndent() - jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) - JBCefJSQuery.Response("success") - } catch (e: Exception) { - e.printStackTrace() - } + jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) + JBCefJSQuery.Response("success") } return@addHandler JBCefJSQuery.Response("success") } From a174e45aed8d3aba5569bd5c96254ab112050152 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 16 Sep 2024 16:15:48 +0200 Subject: [PATCH 23/28] fix: update unit test and remove duplicate code --- .../plugin/ui/jcef/ApplyFixHandlerTest.kt | 94 ++----------------- 1 file changed, 6 insertions(+), 88 deletions(-) diff --git a/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt b/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt index 76725efc1..0177528e9 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt @@ -1,9 +1,9 @@ package io.snyk.plugin.ui.jcef import org.junit.Test -import junit.framework.TestCase.assertEquals +import com.intellij.testFramework.fixtures.BasePlatformTestCase -class DiffPatchTest { +class DiffPatchTest : BasePlatformTestCase(){ private val originalFileContent = """ /* * Copyright (c) 2014-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. @@ -46,8 +46,10 @@ class DiffPatchTest { @Test fun `test applying patch`() { - val diffPatch = parseDiff(responseDiff) - val patchedContent = applyPatch(originalFileContent, diffPatch) + val applyFixHandler = ApplyFixHandler(project) + + val diffPatch = applyFixHandler.parseDiff(responseDiff) + val patchedContent = applyFixHandler.applyPatch(originalFileContent, diffPatch) val expectedPatchedContent = """ /* @@ -76,88 +78,4 @@ class DiffPatchTest { assertEquals(expectedPatchedContent, patchedContent) } - - private 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") - } - - private 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() - var currentHunk: Hunk? = null - val changes = mutableListOf() - - 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 - ) - } } From ee0b923c1ab160ec616141bdd4567f1a3e63c4b5 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 16 Sep 2024 17:02:38 +0200 Subject: [PATCH 24/28] fix: improve error handling and added balloon notifier for failing to apply patch --- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 78 ++++++++++--------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index 5221cb361..a1fa8beaa 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -12,6 +12,8 @@ import org.cef.browser.CefFrame import org.cef.handler.CefLoadHandlerAdapter import org.jetbrains.concurrency.runAsync import io.snyk.plugin.toVirtualFile +import io.snyk.plugin.ui.SnykBalloonNotificationHelper +import java.io.IOException data class DiffPatch( @@ -51,14 +53,32 @@ class ApplyFixHandler(private val project: Project) { // Avoid blocking the UI thread runAsync { - val success = applyPatchAndSave(project, filePath, patch) - - val script = """ - window.receiveApplyFixResponse($success); - """.trimIndent() + //var success = true + 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) + } - jbCefBrowser.cefBrowser.executeJavaScript(script, jbCefBrowser.cefBrowser.url, 0) - JBCefJSQuery.Response("success") + ApplicationManager.getApplication().invokeLater { + 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") } @@ -80,45 +100,33 @@ class ApplyFixHandler(private val project: Project) { } } - private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Boolean { + private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Result { val virtualFile = filePath.toVirtualFile() - val fileContent = ApplicationManager.getApplication().runReadAction { - val document = FileDocumentManager.getInstance().getDocument(virtualFile) - document?.text - } ?: run { - log("[applyPatchAndSave] Document not found or is null for virtual file: $filePath") - return false - } - - val diffPatch = parseDiff(patch) - val patchedContent = applyPatch(fileContent, diffPatch) - - // Apply the patch inside a WriteCommandAction - val result = try { - var appliedPatchSuccessfully = true + return try { WriteCommandAction.runWriteCommandAction(project) { val document = FileDocumentManager.getInstance().getDocument(virtualFile) - if (document != null && fileContent != patchedContent) { - document.setText(patchedContent) - - //temporary logs - logger.info("[applyPatchAndSave] Content after applying patch:${document.text}") - appliedPatchSuccessfully = true + if (document != null) { + val originalContent = document.text + val patchedContent = applyPatch(originalContent, parseDiff(patch)) + if (originalContent != patchedContent) { + document.setText(patchedContent) + } else { + log("[applyPatchAndSave] Patch did not modify content: $filePath") + } } else { - log("[applyPatchAndSave] Failed to find document for saving patched content.") - appliedPatchSuccessfully = false + log("[applyPatchAndSave] Failed to find document for: $filePath") + return@runWriteCommandAction } } - appliedPatchSuccessfully + Result.success(Unit) } catch (e: Exception) { - log("[applyPatchAndSave] Error applying patch: ${e.message}") - false // Failure + log("[applyPatchAndSave] Error applying patch to: $filePath. e: $e") + Result.failure(e) } - - return result } + fun applyPatch(fileContent: String, diffPatch: DiffPatch): String { val lines = fileContent.lines().toMutableList() From 280a979a17131eee669ef50ff2f99486fdba2149 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 16 Sep 2024 17:41:07 +0200 Subject: [PATCH 25/28] tidy: refactor functions to a new patcher --- src/main/kotlin/io/snyk/plugin/DiffPatcher.kt | 94 +++++++++++++++++++ .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 92 +----------------- ...lyFixHandlerTest.kt => DiffPatcherTest.kt} | 12 +-- 3 files changed, 104 insertions(+), 94 deletions(-) create mode 100644 src/main/kotlin/io/snyk/plugin/DiffPatcher.kt rename src/test/kotlin/io/snyk/plugin/{ui/jcef/ApplyFixHandlerTest.kt => DiffPatcherTest.kt} (88%) diff --git a/src/main/kotlin/io/snyk/plugin/DiffPatcher.kt b/src/main/kotlin/io/snyk/plugin/DiffPatcher.kt new file mode 100644 index 000000000..5d4e28006 --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/DiffPatcher.kt @@ -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 { + 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() + var currentHunk: Hunk? = null + val changes = mutableListOf() + + 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 + ) + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index a1fa8beaa..717de21df 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -7,11 +7,12 @@ 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.toVirtualFile import io.snyk.plugin.ui.SnykBalloonNotificationHelper import java.io.IOException @@ -102,13 +103,14 @@ class ApplyFixHandler(private val project: Project) { private fun applyPatchAndSave(project: Project, filePath: String, patch: String): Result { 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 = applyPatch(originalContent, parseDiff(patch)) + val patchedContent = patcher.applyPatch(originalContent, patcher.parseDiff(patch)) if (originalContent != patchedContent) { document.setText(patchedContent) } else { @@ -127,92 +129,6 @@ class ApplyFixHandler(private val project: Project) { } - 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() - var currentHunk: Hunk? = null - val changes = mutableListOf() - - 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 log(logMessage: String) { when { enableDebug -> logger.debug(logMessage) diff --git a/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt b/src/test/kotlin/io/snyk/plugin/DiffPatcherTest.kt similarity index 88% rename from src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt rename to src/test/kotlin/io/snyk/plugin/DiffPatcherTest.kt index 0177528e9..01bc6782d 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandlerTest.kt +++ b/src/test/kotlin/io/snyk/plugin/DiffPatcherTest.kt @@ -1,9 +1,9 @@ -package io.snyk.plugin.ui.jcef +package io.snyk.plugin import org.junit.Test -import com.intellij.testFramework.fixtures.BasePlatformTestCase +import org.junit.Assert.assertEquals -class DiffPatchTest : BasePlatformTestCase(){ +class DiffPatcherTest (){ private val originalFileContent = """ /* * Copyright (c) 2014-2023 Bjoern Kimminich & the OWASP Juice Shop contributors. @@ -46,10 +46,10 @@ class DiffPatchTest : BasePlatformTestCase(){ @Test fun `test applying patch`() { - val applyFixHandler = ApplyFixHandler(project) + val diffPatcher = DiffPatcher() - val diffPatch = applyFixHandler.parseDiff(responseDiff) - val patchedContent = applyFixHandler.applyPatch(originalFileContent, diffPatch) + val diffPatch = diffPatcher.parseDiff(responseDiff) + val patchedContent = diffPatcher.applyPatch(originalFileContent, diffPatch) val expectedPatchedContent = """ /* From f155b48fec3f46090efbb261082477bf3712606a Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 16 Sep 2024 17:58:10 +0200 Subject: [PATCH 26/28] tidy: remove unused parameters --- src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 1 - src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt | 3 +-- .../snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index 717de21df..952835236 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -128,7 +128,6 @@ class ApplyFixHandler(private val project: Project) { } } - private fun log(logMessage: String) { when { enableDebug -> logger.debug(logMessage) 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 93e08c3af..9435e50e9 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -1,7 +1,6 @@ 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 org.cef.browser.CefBrowser @@ -10,7 +9,7 @@ import org.cef.handler.CefLoadHandlerAdapter import org.jetbrains.concurrency.runAsync import snyk.common.lsp.LanguageServerWrapper -class GenerateAIFixHandler(private val project: Project) { +class GenerateAIFixHandler() { fun generateAIFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { val aiFixQuery = JBCefJSQuery.create(jbCefBrowser) 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 667d77f48..95c1751d0 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 @@ -62,7 +62,7 @@ class SuggestionDescriptionPanelFromLS( openFileLoadHandlerGenerator.generate(it) } - val generateAIFixHandler = GenerateAIFixHandler(snykFile.project) + val generateAIFixHandler = GenerateAIFixHandler() loadHandlerGenerators += { generateAIFixHandler.generateAIFixCommand(it) } From 69e7c1ea33a4a61bd3c51d7ce83f8b1547725aac Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Thu, 19 Sep 2024 11:46:47 +0200 Subject: [PATCH 27/28] fix: updating logging and refactor types --- src/main/kotlin/io/snyk/plugin/DiffPatcher.kt | 22 ++++- .../io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 94 ++++++------------- .../plugin/ui/jcef/GenerateAIFixHandler.kt | 4 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 7 -- src/main/kotlin/snyk/common/lsp/Types.kt | 5 + 5 files changed, 58 insertions(+), 74 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/DiffPatcher.kt b/src/main/kotlin/io/snyk/plugin/DiffPatcher.kt index 5d4e28006..4eb2aee14 100644 --- a/src/main/kotlin/io/snyk/plugin/DiffPatcher.kt +++ b/src/main/kotlin/io/snyk/plugin/DiffPatcher.kt @@ -1,8 +1,24 @@ 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 +data class DiffPatch( + val originalFile: String, + val fixedFile: String, + val hunks: List +) + +data class Hunk( + val startLineOriginal: Int, + val numLinesOriginal: Int, + val startLineFixed: Int, + val numLinesFixed: Int, + val changes: List +) + +sealed class Change { + 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 DiffPatcher { diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index 952835236..53bc85659 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -1,7 +1,7 @@ package io.snyk.plugin.ui.jcef -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.diagnostic.LogLevel import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.Project @@ -14,34 +14,17 @@ import org.cef.browser.CefFrame import org.cef.handler.CefLoadHandlerAdapter import org.jetbrains.concurrency.runAsync import io.snyk.plugin.ui.SnykBalloonNotificationHelper +import snyk.common.lsp.LanguageServerWrapper import java.io.IOException - -data class DiffPatch( - val originalFile: String, - val fixedFile: String, - val hunks: List -) - -data class Hunk( - val startLineOriginal: Int, - val numLinesOriginal: Int, - val startLineFixed: Int, - val numLinesFixed: Int, - val changes: List -) - -sealed class Change { - 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) + val logger = Logger.getInstance(this::class.java).apply { + // tie log level to language server log level + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) + if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + } fun generateApplyFixCommand(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { @@ -54,32 +37,30 @@ class ApplyFixHandler(private val project: Project) { // Avoid blocking the UI thread runAsync { - //var success = true val result = try { applyPatchAndSave(project, filePath, patch) } catch (e: IOException) { // Catch specific file-related exceptions - log("Error applying patch to file: $filePath. e:$e") + logger.error("Error applying patch to file: $filePath. e:$e") Result.failure(e) } catch (e: Exception) { - log("Unexpected error applying patch. e:$e") + logger.error("Unexpected error applying patch. e:$e") Result.failure(e) } - ApplicationManager.getApplication().invokeLater { - if (result.isSuccess) { - val script = """ + 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 = """ + 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) - } + jbCefBrowser.cefBrowser.executeJavaScript(errorScript, jbCefBrowser.cefBrowser.url, 0) } + } return@addHandler JBCefJSQuery.Response("success") } @@ -105,34 +86,21 @@ class ApplyFixHandler(private val project: Project) { 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") - } + 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] Failed to find document for: $filePath") - return@runWriteCommandAction + logger.warn("[applyPatchAndSave] Patch did not modify content: $filePath") } + } else { + logger.error("[applyPatchAndSave] Failed to find document for: $filePath") + return@runWriteCommandAction } - Result.success(Unit) - } catch (e: Exception) { - log("[applyPatchAndSave] Error applying patch to: $filePath. e: $e") - Result.failure(e) - } - } - - private fun log(logMessage: String) { - when { - enableDebug -> logger.debug(logMessage) - enableTrace -> logger.trace(logMessage) - else -> logger.error(logMessage) } + return Result.success(Unit) } } 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 9435e50e9..d430c2ae7 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -8,6 +8,8 @@ import org.cef.browser.CefFrame import org.cef.handler.CefLoadHandlerAdapter import org.jetbrains.concurrency.runAsync import snyk.common.lsp.LanguageServerWrapper +import snyk.common.lsp.Fix + class GenerateAIFixHandler() { @@ -22,7 +24,7 @@ class GenerateAIFixHandler() { runAsync { - val responseDiff: List = + val responseDiff: List = LanguageServerWrapper.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID) val script = """ diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 1bbb72f05..13dbfdffc 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -1,8 +1,6 @@ package snyk.common.lsp import com.google.gson.Gson -import com.google.gson.annotations.SerializedName -import com.google.gson.reflect.TypeToken import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service @@ -497,11 +495,6 @@ 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 { if (!ensureLanguageServerInitialized()) return emptyList() diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 3553a3305..fe88f5c35 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -337,6 +337,11 @@ data class ExampleCommitFix( @SerializedName("lines") val lines: List, ) +data class Fix( + @SerializedName("fixId") val fixId: String, + @SerializedName("unifiedDiffsPerFile") val unifiedDiffsPerFile: Map +) + data class CommitChangeLine( @SerializedName("line") val line: String, @SerializedName("lineNumber") val lineNumber: Int, From 2d00d294353da6e4ddbd178550cc4618bd55abc1 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Fri, 20 Sep 2024 11:02:54 +0200 Subject: [PATCH 28/28] fix: add balloon notifier for file not found --- src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt index 53bc85659..c397ecd1f 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ApplyFixHandler.kt @@ -98,6 +98,8 @@ class ApplyFixHandler(private val project: Project) { } } else { logger.error("[applyPatchAndSave] Failed to find document for: $filePath") + val errorMessage = "Failed to find document for: $filePath" + SnykBalloonNotificationHelper.showError(errorMessage, project) return@runWriteCommandAction } }