diff --git a/build.gradle.kts b/build.gradle.kts index 37e1c8d12..79c7ae9c1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,7 @@ repositories { dependencies { implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0")) implementation(platform("com.squareup.retrofit2:retrofit-bom:2.11.0")) - implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.22.0") + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.1") implementation("org.commonmark:commonmark:0.21.0") implementation("com.google.code.gson:gson:2.10.1") 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 7e85c28af..eca2af9ba 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/GenerateAIFixHandler.kt @@ -17,7 +17,7 @@ class GenerateAIFixHandler() { val aiFixQuery = JBCefJSQuery.create(jbCefBrowser) aiFixQuery.addHandler { value -> - val params = value.split(":") + val params = value.split("@|@") val folderURI = params[0] val fileURI = params[1] val issueID = params[2] @@ -54,15 +54,15 @@ class GenerateAIFixHandler() { const filePath = aiFixButton.getAttribute('file-path'); aiFixButton.addEventListener('click', () => { - window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); + window.aiFixQuery(folderPath + "@|@" + filePath + "@|@" + issueId); }); retryFixButton.addEventListener('click', () => { - window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); + window.aiFixQuery(folderPath + "@|@" + filePath + "@|@" + issueId); }); retryFixButton.addEventListener('click', () => { - window.aiFixQuery(folderPath + ":" + filePath + ":" + issueId); + window.aiFixQuery(folderPath + "@|@" + filePath + "@|@" + issueId); }); })(); """ diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index dc37fba2b..a861f1507 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -65,6 +65,7 @@ import io.snyk.plugin.ui.toolwindow.panels.StatePanel import io.snyk.plugin.ui.toolwindow.panels.TreePanel import io.snyk.plugin.ui.wrapWithScrollPane import org.jetbrains.annotations.TestOnly +import org.jetbrains.concurrency.runAsync import snyk.common.ProductType import snyk.common.SnykError import snyk.common.lsp.LanguageServerWrapper @@ -79,6 +80,7 @@ import java.awt.BorderLayout import java.util.Objects.nonNull import javax.swing.JPanel import javax.swing.JScrollPane +import javax.swing.event.TreeSelectionEvent import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultTreeModel import javax.swing.tree.TreePath @@ -93,7 +95,7 @@ class SnykToolWindowPanel( Disposable { private val descriptionPanel = SimpleToolWindowPanel(true, true).apply { name = "descriptionPanel" } private val logger = Logger.getInstance(this::class.java) - private val rootTreeNode = ChooseBranchNode(project = project) + private val rootTreeNode = ChooseBranchNode(project = project) private val rootOssTreeNode = RootOssTreeNode(project) private val rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) private val rootQualityIssuesTreeNode = RootQualityIssuesTreeNode(project) @@ -129,10 +131,10 @@ class SnykToolWindowPanel( } - init { val folderConfig = service().getFolderConfig(project.basePath.toString()) - val rootNodeText = folderConfig?.let { getRootNodeText(it.folderPath, it.baseBranch) } ?: "Choose branch on ${project.basePath}" + val rootNodeText = folderConfig?.let { getRootNodeText(it.folderPath, it.baseBranch) } + ?: "Choose branch on ${project.basePath}" rootTreeNode.info = rootNodeText vulnerabilitiesTree.cellRenderer = SnykTreeCellRenderer() @@ -148,8 +150,10 @@ class SnykToolWindowPanel( createTreeAndDescriptionPanel() chooseMainPanelToDisplay() - vulnerabilitiesTree.selectionModel.addTreeSelectionListener { - updateDescriptionPanelBySelectedTreeNode() + vulnerabilitiesTree.selectionModel.addTreeSelectionListener { treeSelectionEvent -> + runAsync { + updateDescriptionPanelBySelectedTreeNode(treeSelectionEvent) + } } val scanListenerLS = @@ -316,45 +320,74 @@ class SnykToolWindowPanel( ) } - private fun updateDescriptionPanelBySelectedTreeNode() { + private fun updateDescriptionPanelBySelectedTreeNode(treeSelectionEvent: TreeSelectionEvent) { val capturedSmartReloadMode = smartReloadMode val capturedNavigateToSourceEnabled = triggerSelectionListeners - ApplicationManager.getApplication().invokeLater { - descriptionPanel.removeAll() - val selectionPath = vulnerabilitiesTree.selectionPath - if (nonNull(selectionPath)) { - val lastPathComponent = selectionPath!!.lastPathComponent + val selectionPath = treeSelectionEvent.path + if (nonNull(selectionPath) && treeSelectionEvent.isAddedPath) { + val lastPathComponent = selectionPath.lastPathComponent - if (lastPathComponent is ChooseBranchNode && capturedNavigateToSourceEnabled && !capturedSmartReloadMode) { + if (lastPathComponent is ChooseBranchNode && capturedNavigateToSourceEnabled && !capturedSmartReloadMode) { + invokeLater { BranchChooserComboBoxDialog(project).show() } + } - if (!capturedSmartReloadMode && - capturedNavigateToSourceEnabled && - lastPathComponent is NavigatableToSourceTreeNode - ) { - lastPathComponent.navigateToSource() - } - when (val selectedNode: DefaultMutableTreeNode = lastPathComponent as DefaultMutableTreeNode) { - is DescriptionHolderTreeNode -> { + if (!capturedSmartReloadMode && + capturedNavigateToSourceEnabled && + lastPathComponent is NavigatableToSourceTreeNode + ) { + lastPathComponent.navigateToSource() + } + when (val selectedNode: DefaultMutableTreeNode = lastPathComponent as DefaultMutableTreeNode) { + is DescriptionHolderTreeNode -> { + if (selectedNode is SuggestionTreeNode) { + val cache = getSnykCachedResults(project) ?: return + val issue = selectedNode.issue + val productIssues = when (issue.filterableIssueType) { + ScanIssue.CODE_SECURITY, ScanIssue.CODE_QUALITY -> cache.currentSnykCodeResultsLS + ScanIssue.OPEN_SOURCE -> cache.currentOSSResultsLS + ScanIssue.INFRASTRUCTURE_AS_CODE -> cache.currentIacResultsLS + ScanIssue.CONTAINER -> cache.currentContainerResultsLS + else -> { + emptyMap() + } + } + productIssues.values.flatten().filter { issue.id == it.id }.forEach { _ -> + val newDescriptionPanel = selectedNode.getDescriptionPanel() + descriptionPanel.removeAll() + descriptionPanel.add( + newDescriptionPanel, + BorderLayout.CENTER, + ) + } + } else { + descriptionPanel.removeAll() descriptionPanel.add( selectedNode.getDescriptionPanel(), BorderLayout.CENTER, ) } - is ErrorHolderTreeNode -> { - selectedNode.getSnykError()?.let { - displaySnykError(it) - } ?: displayEmptyDescription() - } + } - else -> displayEmptyDescription() + is ErrorHolderTreeNode -> { + descriptionPanel.removeAll() + selectedNode.getSnykError()?.let { + displaySnykError(it) + } ?: displayEmptyDescription() + } + + else -> { + descriptionPanel.removeAll() + displayEmptyDescription() } - } else { - displayEmptyDescription() } + } else { + displayEmptyDescription() + } + invokeLater { descriptionPanel.revalidate() descriptionPanel.repaint() } @@ -367,7 +400,7 @@ class SnykToolWindowPanel( } fun cleanUiAndCaches() { - getSnykCachedResults(project)?.cleanCaches() + getSnykCachedResults(project)?.clearCaches() rootOssTreeNode.originalCliErrorMessage = null getKubernetesImageCache(project)?.let { @@ -842,9 +875,6 @@ class SnykToolWindowPanel( smartReloadMode = true try { selectedNode?.let { TreeUtil.selectNode(vulnerabilitiesTree, it) } - // for some reason TreeSelectionListener is not initiated here on node selection - // also we need to update Description panel in case if no selection was made before - updateDescriptionPanelBySelectedTreeNode() } finally { smartReloadMode = false } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index 517c0a6c9..211203d54 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -13,6 +13,7 @@ import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles +import io.snyk.plugin.ui.expandTreeNodeRecursively import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_QUALITY_ROOT_TEXT import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_SECURITY_ROOT_TEXT import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.IAC_ROOT_TEXT @@ -62,6 +63,7 @@ class SnykToolWindowSnykScanListenerLS( override fun scanningStarted(snykScan: SnykScanParams) { if (disposed) return ApplicationManager.getApplication().invokeLater { + this.snykToolWindowPanel.cleanUiAndCaches() this.snykToolWindowPanel.updateTreeRootNodesPresentation() this.snykToolWindowPanel.displayScanningMessage() } @@ -336,16 +338,12 @@ class SnykToolWindowSnykScanListenerLS( } val settings = pluginSettings() - var text = "✅ Congrats! No vulnerabilities found!" + var text = "✅ Congrats! No issues found!" val issuesCount = issues.size val ignoredIssuesCount = issues.count { it.isIgnored() } if (issuesCount != 0) { - val plural = if (issuesCount == 1) { - "y" - } else { - "ies" - } - text = "✋ $issuesCount vulnerabilit$plural found by Snyk" + val plural = getPlural(issuesCount) + text = "✋ $issuesCount issue$plural found by Snyk" if (pluginSettings().isGlobalIgnoresFeatureEnabled) { text += ", $ignoredIssuesCount ignored" } @@ -359,15 +357,16 @@ class SnykToolWindowSnykScanListenerLS( if (fixableIssuesCount != null) { if (fixableIssuesCount > 0) { + val plural = getPlural(fixableIssuesCount) rootNode.add( InfoTreeNode( - "⚡ $fixableIssuesCount vulnerabilities can be fixed automatically", + "⚡ $fixableIssuesCount issue$plural can be fixed automatically", project, ), ) } else { rootNode.add( - InfoTreeNode("There are no vulnerabilities automatically fixable", project), + InfoTreeNode("There are no issues automatically fixable", project), ) } } @@ -390,6 +389,12 @@ class SnykToolWindowSnykScanListenerLS( } } + private fun getPlural(issuesCount: Int) = if (issuesCount > 1) { + "s" + } else { + "" + } + private fun displayResultsForRootTreeNode( rootNode: DefaultMutableTreeNode, issues: Map>, @@ -422,12 +427,14 @@ class SnykToolWindowSnykScanListenerLS( .forEach { issue -> fileTreeNode.add( SuggestionTreeNode( + project, issue, navigateToSource(entry.key.virtualFile, issue.textRange ?: TextRange(0, 0)), ), ) } } + expandTreeNodeRecursively(snykToolWindowPanel.vulnerabilitiesTree, rootNode) } private fun buildSeveritiesPostfixForFileNode(results: Map>): String { diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/SuggestionTreeNode.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/SuggestionTreeNode.kt index d6c4e60a6..aef802c4e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/SuggestionTreeNode.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/leaf/SuggestionTreeNode.kt @@ -1,29 +1,20 @@ package io.snyk.plugin.ui.toolwindow.nodes.leaf -import io.snyk.plugin.SnykFile +import com.intellij.openapi.project.Project import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode import io.snyk.plugin.ui.toolwindow.nodes.NavigatableToSourceTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode import io.snyk.plugin.ui.toolwindow.panels.IssueDescriptionPanelBase import io.snyk.plugin.ui.toolwindow.panels.SuggestionDescriptionPanelFromLS -import snyk.common.ProductType import snyk.common.lsp.ScanIssue import javax.swing.tree.DefaultMutableTreeNode class SuggestionTreeNode( - private val issue: ScanIssue, + val project: Project, + val issue: ScanIssue, override val navigateToSource: () -> Unit ) : DefaultMutableTreeNode(issue), NavigatableToSourceTreeNode, DescriptionHolderTreeNode { - @Suppress("UNCHECKED_CAST") override fun getDescriptionPanel(): IssueDescriptionPanelBase { - val snykFileTreeNode = this.parent as? SnykFileTreeNode - ?: throw IllegalArgumentException(this.toString()) - - @Suppress("UNCHECKED_CAST") - val entry = - (snykFileTreeNode.userObject as Pair>, ProductType>).first - val snykFile = entry.key - return SuggestionDescriptionPanelFromLS(snykFile, issue) + return SuggestionDescriptionPanelFromLS(project, issue) } } 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 58d656289..30c58e465 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 @@ -2,11 +2,11 @@ package io.snyk.plugin.ui.toolwindow.panels import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.uiDesigner.core.GridLayoutManager import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil -import io.snyk.plugin.SnykFile import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.DescriptionHeaderPanel import io.snyk.plugin.ui.SnykBalloonNotificationHelper @@ -31,13 +31,12 @@ import javax.swing.JPanel import kotlin.collections.set class SuggestionDescriptionPanelFromLS( - snykFile: SnykFile, + val project: Project, private val issue: ScanIssue, ) : IssueDescriptionPanelBase( 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." @@ -57,7 +56,7 @@ class SuggestionDescriptionPanelFromLS( virtualFiles[dataFlow.filePath] = dataFlow.filePath.toVirtualFile() } - val openFileLoadHandlerGenerator = OpenFileLoadHandlerGenerator(snykFile.project, virtualFiles) + val openFileLoadHandlerGenerator = OpenFileLoadHandlerGenerator(project, virtualFiles) loadHandlerGenerators += { openFileLoadHandlerGenerator.generate(it) } @@ -67,7 +66,7 @@ class SuggestionDescriptionPanelFromLS( generateAIFixHandler.generateAIFixCommand(it) } - val applyFixHandler = ApplyFixHandler(snykFile.project) + val applyFixHandler = ApplyFixHandler(project) loadHandlerGenerators += { applyFixHandler.generateApplyFixCommand(it) } @@ -161,7 +160,6 @@ class SuggestionDescriptionPanelFromLS( // TODO: remove custom stylesheets, deliver variables from LS, replace variables with colors val ideStyle: String = when (issue.filterableIssueType) { ScanIssue.CODE_SECURITY, ScanIssue.CODE_QUALITY -> SnykStylesheets.SnykCodeSuggestion - ScanIssue.OPEN_SOURCE -> SnykStylesheets.SnykOSSSuggestion else -> "" } @@ -178,17 +176,27 @@ class SuggestionDescriptionPanelFromLS( html = html.replace("--default-font: ", "--default-font: \"${JBUI.Fonts.label().asPlain().family}\", ") html = html.replace("var(--text-color)", UIUtil.getLabelForeground().toHex()) html = html.replace("var(--background-color)", UIUtil.getPanelBackground().toHex()) - html = - html.replace("var(--border-color)", JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground().toHex()) + val borderColor = JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground().toHex() + html = html.replace("var(--border-color)", borderColor) + html = html.replace("var(--horizontal-border-color)", borderColor) html = html.replace("var(--link-color)", JBUI.CurrentTheme.Link.Foreground.ENABLED.toHex()) + + val editorBackground = + editorUiTheme.getColor(EditorColors.GUTTER_BACKGROUND)?.toHex() ?: editorUiTheme.defaultBackground.toHex() html = html.replace( - "var(--horizontal-border-color)", - JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground().toHex() + "var(--code-background-color)", + editorBackground ) html = html.replace( - "var(--code-background-color)", - editorUiTheme.getColor(EditorColors.GUTTER_BACKGROUND)?.toHex() ?: editorUiTheme.defaultBackground.toHex() + "var(--container-background-color)", + editorBackground ) + + html = html.replace( + "var(--editor-color)", + editorBackground + ) + return html } diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index cf6813abe..5b580a9cb 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -35,7 +35,7 @@ class SnykCachedResults( override fun dispose() { disposed = true - cleanCaches() + clearCaches() } fun isDisposed() = disposed @@ -54,7 +54,7 @@ class SnykCachedResults( var currentIacError: SnykError? = null var currentSnykCodeError: SnykError? = null - fun cleanCaches() { + fun clearCaches() { currentContainerResult = null currentOssError = null currentContainerError = null diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 0e8464c18..e7a1da038 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -20,9 +20,6 @@ import io.snyk.plugin.pluginSettings import io.snyk.plugin.runInBackground import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import org.eclipse.lsp4j.ClientCapabilities import org.eclipse.lsp4j.ClientInfo import org.eclipse.lsp4j.CodeActionCapabilities @@ -42,6 +39,7 @@ import org.eclipse.lsp4j.WorkspaceEditCapabilities import org.eclipse.lsp4j.WorkspaceFolder import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent import org.eclipse.lsp4j.jsonrpc.Launcher +import org.eclipse.lsp4j.jsonrpc.MessageConsumer import org.eclipse.lsp4j.jsonrpc.RemoteEndpoint import org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer import org.eclipse.lsp4j.launch.LSPLauncher @@ -82,7 +80,7 @@ private const val INITIALIZATION_TIMEOUT = 20L @Suppress("TooGenericExceptionCaught") class LanguageServerWrapper( private val lsPath: String = getCliFile().absolutePath, - private val executorService: ExecutorService = Executors.newCachedThreadPool(), + private val executorService: ExecutorService = Executors.newCachedThreadPool() ) : Disposable { private var authenticatedUser: Map? = null private var initializeResult: InitializeResult? = null @@ -120,7 +118,6 @@ class LanguageServerWrapper( var isInitialized: Boolean = false - @OptIn(DelicateCoroutinesApi::class) private fun initialize() { if (disposed) return if (lsPath.toNioPathOrNull()?.exists() == false) { @@ -143,7 +140,8 @@ class LanguageServerWrapper( EnvironmentHelper.updateEnvironment(processBuilder.environment(), pluginSettings().token ?: "") process = processBuilder.start() - GlobalScope.launch { + + runAsync { if (!disposed) { try { process.errorStream.bufferedReader().forEachLine { logger.debug(it) } @@ -153,21 +151,38 @@ class LanguageServerWrapper( } } - launcher = LSPLauncher.createClientLauncher(languageClient, process.inputStream, process.outputStream) + // enable message logging + val wrapper = fun(wrapped: MessageConsumer): MessageConsumer { + return MessageConsumer { message -> + logger.trace(message.toString()) + wrapped.consume(message) + } + } + + launcher = LSPLauncher.createClientLauncher( + snykLanguageClient, + process.inputStream, + process.outputStream, + executorService, + wrapper + ) + languageServer = launcher.remoteProxy val listenerFuture = launcher.startListening() runAsync { listenerFuture.get() + logger.info("Snyk Language Server was terminated, listener has ended.") isInitialized = false } - if (!listenerFuture.isDone) { + if (!(listenerFuture.isDone || listenerFuture.isCancelled)) { sendInitializeMessage() isInitialized = true // listen for downloads / restarts LanguageServerRestartListener.getInstance() + refreshFeatureFlags() } else { logger.warn("Language Server initialization did not succeed") } @@ -365,7 +380,7 @@ class LanguageServerWrapper( } } - private fun getFeatureFlagStatus(featureFlag: String): Boolean { + private fun getFeatureFlagStatus(@Suppress("SameParameterValue") featureFlag: String): Boolean { if (notAuthenticated()) return false try { val param = ExecuteCommandParams() @@ -525,19 +540,26 @@ class LanguageServerWrapper( } } - fun generateIssueDescription(issueID: String): String? { + fun generateIssueDescription(issue: ScanIssue): String? { if (!ensureLanguageServerInitialized()) return null - try { - val generateIssueCommand = ExecuteCommandParams(SNYK_GENERATE_ISSUE_DESCRIPTION, listOf(issueID)) - val result = - languageServer.workspaceService - .executeCommand(generateIssueCommand) - .get(2, TimeUnit.SECONDS) - .toString() - return result + val key = issue.additionalData.key + if (key.isBlank()) throw RuntimeException("Issue ID is required") + val generateIssueCommand = ExecuteCommandParams(SNYK_GENERATE_ISSUE_DESCRIPTION, listOf(key)) + return try { + executeCommand(generateIssueCommand, Long.MAX_VALUE).toString() } catch (e: TimeoutException) { - logger.warn("could not generate html description", e) - return null + val exceptionMessage = "generate issue description failed" + logger.warn(exceptionMessage, e) + null + } catch (e: Exception) { + if (e.message?.contains("failed to find issue") == true) { + val msg = "The issue is not in the server cache anymore, please wait for any running scans to finish" + logger.debug(msg) + return msg + } else { + logger.error("generate issue description failed", e) + null + } } } @@ -545,7 +567,7 @@ class LanguageServerWrapper( if (!ensureLanguageServerInitialized()) return val cmd = ExecuteCommandParams(COMMAND_LOGOUT, emptyList()) try { - languageServer.workspaceService.executeCommand(cmd).get(5, TimeUnit.SECONDS) + executeCommand(cmd) } catch (e: TimeoutException) { logger.warn("could not logout", e) } @@ -567,7 +589,7 @@ 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 List<*> + val result = executeCommand(param, 120000) as List<*> val diffList: MutableList = mutableListOf() result.forEach { @@ -595,7 +617,7 @@ class LanguageServerWrapper( val param = ExecuteCommandParams() param.command = COMMAND_CODE_SUBMIT_FIX_FEEDBACK param.arguments = listOf(fixId, feedback) - languageServer.workspaceService.executeCommand(param) + executeCommand(param) } catch (err: Exception) { logger.warn("Error in submitAutofixFeedbackCommand", err) } @@ -631,8 +653,7 @@ class LanguageServerWrapper( if (!ensureLanguageServerInitialized()) return null try { val executeCommandParams = ExecuteCommandParams(COMMAND_GET_SETTINGS_SAST_ENABLED, emptyList()) - val response = - languageServer.workspaceService.executeCommand(executeCommandParams).get(10, TimeUnit.SECONDS) + val response = executeCommand(executeCommandParams, 10000) if (response is Map<*, *>) { val localCodeEngineMap: Map = response["localCodeEngine"] as Map return SastSettings( @@ -686,4 +707,3 @@ class LanguageServerWrapper( } } - diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 93b758c82..cfcac6bf9 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -238,7 +238,6 @@ class SnykLanguageClient : when (snykScan.product) { "oss" -> scanPublisher.scanningOssFinished() "code" -> { - LanguageServerWrapper.getInstance().refreshFeatureFlags() scanPublisher.scanningSnykCodeFinished() } "iac" -> scanPublisher.scanningIacFinished() @@ -270,11 +269,12 @@ class SnykLanguageClient : @JsonNotification(value = "$/snyk.hasAuthenticated") fun hasAuthenticated(param: HasAuthenticatedParam) { if (disposed) return - if (pluginSettings().token == param.token) return + val oldToken = pluginSettings().token + if (oldToken == param.token) return pluginSettings().token = param.token ApplicationManager.getApplication().saveSettings() - if (pluginSettings().token?.isNotEmpty() == true && pluginSettings().scanOnSave) { + if (oldToken.isNullOrBlank() && !param.token.isNullOrBlank() && pluginSettings().scanOnSave) { val wrapper = LanguageServerWrapper.getInstance() ProjectManager.getInstance().openProjects.forEach { wrapper.sendScanCommand(it) diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 626705b78..b5f951d0a 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -261,10 +261,11 @@ data class ScanIssue( } private fun getHtml(details: String?): String { - if (details.isNullOrEmpty()) { - return LanguageServerWrapper.getInstance().generateIssueDescription(this.id) ?: "" + return if (details.isNullOrEmpty() && this.id.isNotBlank()) { + LanguageServerWrapper.getInstance().generateIssueDescription(this) ?: "" + } else { + "" } - return details } fun annotationMessage(): String { diff --git a/src/main/resources/stylesheets/snyk_code_suggestion.scss b/src/main/resources/stylesheets/snyk_code_suggestion.scss index ba2c6c3ef..6d2dda653 100644 --- a/src/main/resources/stylesheets/snyk_code_suggestion.scss +++ b/src/main/resources/stylesheets/snyk_code_suggestion.scss @@ -8,7 +8,6 @@ html, body { height: 100%; - font-size: 16px; display: flex; flex-direction: column; margin: 0; @@ -18,7 +17,6 @@ html, body { body { color: var(--text-color); font-weight: 400; - font-size: 0.875rem; } a, @@ -52,11 +50,9 @@ a, .ai-fix-header, .example-fixes-header, .overview-text > h2 { - font-size: 0.85rem; } .severity-type-container { - font-size: 0.85rem; } .data-flow-clickable-row { @@ -68,7 +64,6 @@ a, } .tab-item { - font-size: 0.85rem; } .tab-item-icon path { @@ -83,10 +78,6 @@ a, border-bottom: 3px solid #3474f0; } -.example-line { - background-color: var(--editor-color); -} - .example-line.added { background-color: var(--example-line-added-color); } diff --git a/src/main/resources/stylesheets/snyk_oss_suggestion.scss b/src/main/resources/stylesheets/snyk_oss_suggestion.scss deleted file mode 100644 index 810ce5ce2..000000000 --- a/src/main/resources/stylesheets/snyk_oss_suggestion.scss +++ /dev/null @@ -1,69 +0,0 @@ -::-webkit-scrollbar-thumb { - background: var(--scrollbar-thumb-color); -} - -::-webkit-scrollbar-thumb:hover { - background: #595a5c; -} - -html, body { - height: 100%; - font-size: 16px; - display: flex; - flex-direction: column; - margin: 0; - padding: 0; -} - -body { - color: var(--text-color); - font-weight: 400; - font-size: 0.875rem; -} - -.font-light { - font-weight: bold; -} - -a, -.link { - color: var(--link-color); -} - -.delimiter { - border-right: 1px solid var(--border-color); -} - -.suggestion--header { - padding-top: 10px; -} - -.suggestion .suggestion-text { - font-size: 1.2rem; - position: relative; - top: -5%; -} - -.summary .summary-item { - margin-bottom: 0.8em; -} - -.summary .label { - font-size: 0.8rem; -} - -.suggestion--header > h2, -.summary > h2, -.vulnerability-overview > h2 { - font-size: 0.9rem; - margin-bottom: 1.5em; -} - -.identifiers { - padding-bottom: 20px; -} - -.vulnerability-overview pre { - 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/SnykToolWindowPanelTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt index 90149c534..f4ceef6d1 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt @@ -9,7 +9,6 @@ import io.mockk.every import io.mockk.justRun import io.mockk.mockk import io.mockk.unmockkAll -import io.mockk.verify import io.snyk.plugin.pluginSettings import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.services.SnykTaskQueueService diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 4465d14af..040cb292f 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -174,12 +174,12 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { TestCase.assertEquals(rootTreeNode.children().toList()[1].toString(), " Code Security") TestCase.assertEquals(rootTreeNode.children().toList()[2].toString(), " Code Quality") TestCase.assertEquals( - "✋ 1 vulnerability found by Snyk, 0 ignored", + "✋ 1 issue found by Snyk, 0 ignored", rootTreeNode.children().toList()[4].toString(), ) TestCase.assertEquals( rootTreeNode.children().toList()[5].toString(), - "⚡ 1 vulnerabilities can be fixed automatically", + "⚡ 1 issue can be fixed automatically", ) } 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 512b0a9dd..10655d408 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt @@ -83,7 +83,7 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { every { issue.details() } returns "HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) + cut = SuggestionDescriptionPanelFromLS(project, issue) val actual = getJLabelByText(cut, "Test message") assertNull(actual) @@ -101,7 +101,7 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { every { issue.details() } returns "HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) + cut = SuggestionDescriptionPanelFromLS(project, issue) val actual = getJLabelByText(cut, "Test message") assertNull(actual) @@ -113,7 +113,7 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { fun `test getStyledHTML should inject CSS into the HTML if allowed`() { every { issue.details() } returns "\${ideStyle}HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) + cut = SuggestionDescriptionPanelFromLS(project, issue) val actual = cut.getCustomCssAndScript() assertFalse(actual.contains("\${ideStyle}")) 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 619dacd68..25db01134 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -83,7 +83,7 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { every { issue.details() } returns "HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) + cut = SuggestionDescriptionPanelFromLS(project, issue) val actual = getJLabelByText(cut, "Test message") assertNull(actual) @@ -95,7 +95,7 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { fun `test getStyledHTML should inject CSS into the HTML if allowed`() { every { issue.details() } returns "HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) + cut = SuggestionDescriptionPanelFromLS(project, issue) val actual = cut.getCustomCssAndScript()