diff --git a/CHANGELOG.md b/CHANGELOG.md index 5457ef4f1..abab1d3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Refactors some files so they can be used by more than just Snyk Code - Registry flag for integrating Snyk OSS scans in JetBrains via the LS +## [2.7.18] +### Added +- De-duplicated code which will be used by all products loaded via the Language Server + ## [2.7.18] ### Added - Improved theming in the Code Issue Panel by applying IntelliJ theme colors dynamically to JCEF components. This ensures consistency of UI elements with the rest of the IDE. diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeDataflowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeDataflowPanel.kt new file mode 100644 index 000000000..1a2e0e529 --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeDataflowPanel.kt @@ -0,0 +1,153 @@ +package io.snyk.plugin.ui.toolwindow.panels + +import com.intellij.openapi.project.Project +import com.intellij.ui.HyperlinkLabel +import com.intellij.uiDesigner.core.GridConstraints +import com.intellij.uiDesigner.core.GridLayoutManager +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import io.snyk.plugin.getDocument +import io.snyk.plugin.navigateToSource +import io.snyk.plugin.toVirtualFile +import io.snyk.plugin.ui.baseGridConstraintsAnchorWest +import snyk.common.lsp.DataFlow +import snyk.common.lsp.IssueData +import java.awt.Font +import javax.swing.JPanel +import javax.swing.JTextArea +import javax.swing.event.HyperlinkEvent +import kotlin.math.max + +class SnykCodeDataflowPanel(private val project: Project, codeIssueData: IssueData): JPanel() { + init { + val dataFlow = codeIssueData.dataFlow + + this.layout = GridLayoutManager(1 + dataFlow.size, 1, JBUI.emptyInsets(), -1, 5) + + this.add( + defaultFontLabel("Data Flow - ${dataFlow.size} step${if (dataFlow.size > 1) "s" else ""}", true), + baseGridConstraintsAnchorWest(0) + ) + + this.add( + stepsPanel(dataFlow), + baseGridConstraintsAnchorWest(1) + ) + } + + + private fun stepsPanel(dataflow: List): JPanel { + val panel = JPanel() + panel.layout = GridLayoutManager(dataflow.size, 1, JBUI.emptyInsets(), 0, 0) + panel.background = UIUtil.getTextFieldBackground() + + val maxFilenameLength = dataflow.asSequence() + .filter { it.filePath.isNotEmpty() } + .map { it.filePath.substringAfterLast('/', "").length } + .maxOrNull() ?: 0 + + val allStepPanels = mutableListOf() + dataflow.forEach { flow -> + val stepPanel = stepPanel( + index = flow.position, + flow = flow, + maxFilenameLength = max(flow.filePath.toVirtualFile().name.length, maxFilenameLength), + allStepPanels = allStepPanels + ) + + panel.add( + stepPanel, + baseGridConstraintsAnchorWest( + row = flow.position, + fill = GridConstraints.FILL_BOTH, + indent = 0 + ) + ) + allStepPanels.add(stepPanel) + } + + return panel + } + + private fun stepPanel( + index: Int, + flow: DataFlow, + maxFilenameLength: Int, + allStepPanels: MutableList + ): JPanel { + val stepPanel = JPanel() + stepPanel.layout = GridLayoutManager(1, 3, JBUI.insetsBottom(4), 0, 0) + stepPanel.background = UIUtil.getTextFieldBackground() + + val paddedStepNumber = (index + 1).toString().padStart(2, ' ') + + val virtualFile = flow.filePath.toVirtualFile() + val fileName = virtualFile.name + + val lineNumber = flow.flowRange.start.line + 1 + val positionLinkText = "$fileName:$lineNumber".padEnd(maxFilenameLength + 5, ' ') + + val positionLabel = linkLabel( + beforeLinkText = "$paddedStepNumber ", + linkText = positionLinkText, + afterLinkText = " |", + toolTipText = "Click to show in the Editor", + customFont = JTextArea().font + ) { + if (!virtualFile.isValid) return@linkLabel + + val document = virtualFile.getDocument() + val startLineStartOffset = document?.getLineStartOffset(flow.flowRange.start.line) ?: 0 + val startOffset = startLineStartOffset + (flow.flowRange.start.character) + val endLineStartOffset = document?.getLineStartOffset(flow.flowRange.end.line) ?: 0 + val endOffset = endLineStartOffset + flow.flowRange.end.character - 1 + + navigateToSource(this.project, virtualFile, startOffset, endOffset) + + allStepPanels.forEach { + it.background = UIUtil.getTextFieldBackground() + } + stepPanel.background = UIUtil.getTableSelectionBackground(false) + } + stepPanel.add(positionLabel, baseGridConstraintsAnchorWest(0, indent = 1)) + + val codeLine = codeLine(flow.content.trimStart()) + codeLine.isOpaque = false + stepPanel.add( + codeLine, + baseGridConstraintsAnchorWest( + row = 0, + // is needed to avoid center alignment when outer panel is filling horizontal space + hSizePolicy = GridConstraints.SIZEPOLICY_CAN_GROW, + column = 1 + ) + ) + + return stepPanel + } + + private fun linkLabel( + beforeLinkText: String = "", + linkText: String, + afterLinkText: String = "", + toolTipText: String, + customFont: Font? = null, + onClick: (HyperlinkEvent) -> Unit + ): HyperlinkLabel { + return HyperlinkLabel().apply { + this.setTextWithHyperlink("$beforeLinkText$linkText$afterLinkText") + this.toolTipText = toolTipText + this.font = io.snyk.plugin.ui.getFont(-1, 14, customFont ?: font) + addHyperlinkListener { + onClick.invoke(it) + } + } + } + + private fun codeLine(content: String): JTextArea { + val component = JTextArea(content) + component.font = io.snyk.plugin.ui.getFont(-1, 14, component.font) + component.isEditable = false + return component + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeExampleFixesPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeExampleFixesPanel.kt new file mode 100644 index 000000000..250842706 --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeExampleFixesPanel.kt @@ -0,0 +1,151 @@ +package io.snyk.plugin.ui.toolwindow.panels + +import com.intellij.icons.AllIcons +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.components.JBTabbedPane +import com.intellij.uiDesigner.core.GridConstraints +import com.intellij.uiDesigner.core.GridLayoutManager +import com.intellij.util.ui.JBInsets +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import io.snyk.plugin.ui.baseGridConstraintsAnchorWest +import io.snyk.plugin.ui.panelGridConstraints +import snyk.common.lsp.IssueData +import java.awt.Color +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.JTextArea +import javax.swing.ScrollPaneConstants + +class SnykCodeExampleFixesPanel(codeIssueData: IssueData): JPanel() { + init { + val fixes = codeIssueData.exampleCommitFixes + val examplesCount = fixes.size.coerceAtMost(3) + + this.layout = GridLayoutManager(3, 1, JBUI.emptyInsets(), -1, 5) + + this.add( + defaultFontLabel("External example fixes", true), + baseGridConstraintsAnchorWest(0) + ) + + val labelText = + if (examplesCount == 0) { + "No example fixes available." + } else { + "This issue was fixed by ${codeIssueData.repoDatasetSize} projects. Here are $examplesCount example fixes." + } + this.add( + defaultFontLabel(labelText), + baseGridConstraintsAnchorWest(1) + ) + + if (examplesCount != 0) { + val tabbedPane = JBTabbedPane() + // tabbedPane.tabLayoutPolicy = JTabbedPane.SCROLL_TAB_LAYOUT // tabs in one row + tabbedPane.tabComponentInsets = JBInsets.create(0, 0) // no inner borders for tab content + tabbedPane.font = io.snyk.plugin.ui.getFont(-1, 14, tabbedPane.font) + + val tabbedPanel = JPanel() + tabbedPanel.layout = GridLayoutManager(1, 1, JBUI.insetsTop(10), -1, -1) + tabbedPanel.add(tabbedPane, panelGridConstraints(0)) + + this.add(tabbedPanel, panelGridConstraints(2, indent = 1)) + + val maxRowCount = fixes.take(examplesCount).maxOfOrNull { it.lines.size } ?: 0 + fixes.take(examplesCount).forEach { exampleCommitFix -> + val shortURL = exampleCommitFix.commitURL + .removePrefix("https://") + .replace(Regex("/commit/.*"), "") + + val tabTitle = shortURL.removePrefix("github.com/").let { + if (it.length > 50) it.take(50) + "..." else it + } + + val icon = if (shortURL.startsWith("github.com")) AllIcons.Vcs.Vendors.Github else null + + tabbedPane.addTab( + tabTitle, + icon, + diffPanel(exampleCommitFix, maxRowCount), + shortURL + ) + } + } + } + + private fun diffPanel(exampleCommitFix: snyk.common.lsp.ExampleCommitFix, rowCount: Int): JComponent { + fun shift(colorComponent: Int, d: Double): Int { + val n = (colorComponent * d).toInt() + return n.coerceIn(0, 255) + } + + val baseColor = UIUtil.getTextFieldBackground() + val addedColor = Color( + shift(baseColor.red, 0.75), + baseColor.green, + shift(baseColor.blue, 0.75) + ) + val removedColor = Color( + shift(baseColor.red, 1.25), + shift(baseColor.green, 0.85), + shift(baseColor.blue, 0.85) + ) + + val panel = JPanel() + panel.layout = GridLayoutManager(rowCount, 1, JBUI.emptyInsets(), -1, 0) + panel.background = baseColor + + exampleCommitFix.lines.forEachIndexed { index, exampleLine -> + val lineText = "%6d %c %s".format( + exampleLine.lineNumber, + when (exampleLine.lineChange) { + "added" -> '+' + "removed" -> '-' + "none" -> ' ' + else -> '!' + }, + exampleLine.line + ) + val codeLine = JTextArea(lineText) + + codeLine.background = when (exampleLine.lineChange) { + "added" -> addedColor + "removed" -> removedColor + "none" -> baseColor + else -> baseColor + } + codeLine.isOpaque = true + codeLine.isEditable = false + codeLine.font = io.snyk.plugin.ui.getFont(-1, 14, codeLine.font) + + panel.add( + codeLine, + baseGridConstraintsAnchorWest( + row = index, + fill = GridConstraints.FILL_BOTH, + indent = 0 + ) + ) + } + + // fill space with empty lines to avoid rows stretching + for (i in exampleCommitFix.lines.size..rowCount) { + val emptyLine = JTextArea("") + panel.add( + emptyLine, + baseGridConstraintsAnchorWest( + row = i - 1, + fill = GridConstraints.FILL_BOTH, + indent = 0 + ) + ) + } + + return ScrollPaneFactory.createScrollPane( + panel, + ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED + ) + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeOverviewPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeOverviewPanel.kt new file mode 100644 index 000000000..17f9f8229 --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SnykCodeOverviewPanel.kt @@ -0,0 +1,24 @@ +package io.snyk.plugin.ui.toolwindow.panels + +import com.intellij.uiDesigner.core.GridLayoutManager +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import io.snyk.plugin.ui.panelGridConstraints +import snyk.common.lsp.IssueData +import java.awt.Dimension +import javax.swing.JComponent +import javax.swing.JLabel + +class SnykCodeOverviewPanel(codeIssueData: IssueData) : JComponent() { + init { + this.layout = GridLayoutManager(2, 1, JBUI.emptyInsets(), -1, -1) + val label = JLabel("" + codeIssueData.message + "").apply { + this.isOpaque = false + this.background = UIUtil.getPanelBackground() + this.font = io.snyk.plugin.ui.getFont(-1, 14, this.font) + this.preferredSize = Dimension() // this is the key part for shrink/grow. + } + this.add(label, panelGridConstraints(1, indent = 1)) + + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SuggestionDescriptionPanelFromLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SuggestionDescriptionPanelFromLS.kt index eb26528af..f5196769a 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SuggestionDescriptionPanelFromLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/SuggestionDescriptionPanelFromLS.kt @@ -1,41 +1,23 @@ package io.snyk.plugin.ui.toolwindow.panels -import com.intellij.icons.AllIcons -import com.intellij.ui.HyperlinkLabel -import com.intellij.ui.ScrollPaneFactory -import com.intellij.ui.components.JBTabbedPane -import com.intellij.uiDesigner.core.GridConstraints import com.intellij.uiDesigner.core.GridLayoutManager -import com.intellij.util.ui.JBInsets import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import io.snyk.plugin.getDocument -import io.snyk.plugin.navigateToSource import io.snyk.plugin.pluginSettings import io.snyk.plugin.SnykFile -import io.snyk.plugin.toVirtualFile 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.JCEFUtils import io.snyk.plugin.ui.jcef.OpenFileLoadHandlerGenerator import io.snyk.plugin.ui.panelGridConstraints import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel import io.snyk.plugin.ui.wrapWithScrollPane -import snyk.common.lsp.DataFlow import snyk.common.lsp.ScanIssue import java.awt.BorderLayout -import java.awt.Color -import java.awt.Dimension import java.awt.Font import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel -import javax.swing.JTextArea -import javax.swing.ScrollPaneConstants -import javax.swing.event.HyperlinkEvent -import kotlin.math.max class SuggestionDescriptionPanelFromLS( snykFile: SnykFile, @@ -89,307 +71,23 @@ class SuggestionDescriptionPanelFromLS( val panel = JPanel( GridLayoutManager(lastRowToAddSpacer + 1, 1, JBUI.insets(0, 10, 20, 10), -1, 20) ).apply { - this.add(overviewPanel(), panelGridConstraints(2)) - - dataFlowPanel().let { this.add(it, panelGridConstraints(3)) } - - this.add(fixExamplesPanel(), panelGridConstraints(4)) + overviewPanel()?.let { this.add(it, panelGridConstraints(2)) } + dataFlowPanel()?.let { this.add(it, panelGridConstraints(3)) } + fixExamplesPanel()?.let { this.add(it, panelGridConstraints(4)) } } return Pair(panel, lastRowToAddSpacer) } - private fun overviewPanel(): JComponent { - val panel = JPanel() - panel.layout = GridLayoutManager(2, 1, JBUI.emptyInsets(), -1, -1) - val label = JLabel("" + getOverviewText() + "").apply { - this.isOpaque = false - this.background = UIUtil.getPanelBackground() - this.font = io.snyk.plugin.ui.getFont(-1, 14, panel.font) - this.preferredSize = Dimension() // this is the key part for shrink/grow. - } - panel.add(label, panelGridConstraints(1, indent = 1)) - - return panel - } - - private fun codeLine(content: String): JTextArea { - val component = JTextArea(content) - component.font = io.snyk.plugin.ui.getFont(-1, 14, component.font) - component.isEditable = false - return component - } - - private fun defaultFontLabel(labelText: String, bold: Boolean = false): JLabel { - return JLabel().apply { - val titleLabelFont: Font? = io.snyk.plugin.ui.getFont(if (bold) Font.BOLD else -1, 14, font) - titleLabelFont?.let { font = it } - text = labelText - } - } - - private fun linkLabel( - beforeLinkText: String = "", - linkText: String, - afterLinkText: String = "", - toolTipText: String, - customFont: Font? = null, - onClick: (HyperlinkEvent) -> Unit - ): HyperlinkLabel { - return HyperlinkLabel().apply { - this.setTextWithHyperlink("$beforeLinkText$linkText$afterLinkText") - this.toolTipText = toolTipText - this.font = io.snyk.plugin.ui.getFont(-1, 14, customFont ?: font) - addHyperlinkListener { - onClick.invoke(it) - } - } - } - - private fun dataFlowPanel(): JPanel { - val dataFlow = issue.additionalData.dataFlow - - val panel = JPanel() - panel.layout = GridLayoutManager(1 + dataFlow.size, 1, JBUI.emptyInsets(), -1, 5) - - panel.add( - defaultFontLabel("Data Flow - ${dataFlow.size} step${if (dataFlow.size > 1) "s" else ""}", true), - baseGridConstraintsAnchorWest(0) - ) - - panel.add( - stepsPanel(dataFlow), - baseGridConstraintsAnchorWest(1) - ) - - return panel - } - - private fun stepsPanel(dataflow: List): JPanel { - val panel = JPanel() - panel.layout = GridLayoutManager(dataflow.size, 1, JBUI.emptyInsets(), 0, 0) - panel.background = UIUtil.getTextFieldBackground() - - val maxFilenameLength = dataflow.asSequence() - .filter { it.filePath.isNotEmpty() } - .map { it.filePath.substringAfterLast('/', "").length } - .maxOrNull() ?: 0 - - val allStepPanels = mutableListOf() - dataflow.forEach { flow -> - val stepPanel = stepPanel( - index = flow.position, - flow = flow, - maxFilenameLength = max(flow.filePath.toVirtualFile().name.length, maxFilenameLength), - allStepPanels = allStepPanels - ) - - panel.add( - stepPanel, - baseGridConstraintsAnchorWest( - row = flow.position, - fill = GridConstraints.FILL_BOTH, - indent = 0 - ) - ) - allStepPanels.add(stepPanel) - } - - return panel - } - - private fun stepPanel( - index: Int, - flow: DataFlow, - maxFilenameLength: Int, - allStepPanels: MutableList - ): JPanel { - val stepPanel = JPanel() - stepPanel.layout = GridLayoutManager(1, 3, JBUI.insetsBottom(4), 0, 0) - stepPanel.background = UIUtil.getTextFieldBackground() - - val paddedStepNumber = (index + 1).toString().padStart(2, ' ') - - val virtualFile = flow.filePath.toVirtualFile() - val fileName = virtualFile.name - - val lineNumber = flow.flowRange.start.line + 1 - val positionLinkText = "$fileName:$lineNumber".padEnd(maxFilenameLength + 5, ' ') - - val positionLabel = linkLabel( - beforeLinkText = "$paddedStepNumber ", - linkText = positionLinkText, - afterLinkText = " |", - toolTipText = "Click to show in the Editor", - customFont = JTextArea().font - ) { - if (!virtualFile.isValid) return@linkLabel - - val document = virtualFile.getDocument() - val startLineStartOffset = document?.getLineStartOffset(flow.flowRange.start.line) ?: 0 - val startOffset = startLineStartOffset + (flow.flowRange.start.character) - val endLineStartOffset = document?.getLineStartOffset(flow.flowRange.end.line) ?: 0 - val endOffset = endLineStartOffset + flow.flowRange.end.character - 1 - - navigateToSource(project, virtualFile, startOffset, endOffset) - - allStepPanels.forEach { - it.background = UIUtil.getTextFieldBackground() - } - stepPanel.background = UIUtil.getTableSelectionBackground(false) - } - stepPanel.add(positionLabel, baseGridConstraintsAnchorWest(0, indent = 1)) - - val codeLine = codeLine(flow.content.trimStart()) - codeLine.isOpaque = false - stepPanel.add( - codeLine, - baseGridConstraintsAnchorWest( - row = 0, - // is needed to avoid center alignment when outer panel is filling horizontal space - hSizePolicy = GridConstraints.SIZEPOLICY_CAN_GROW, - column = 1 - ) - ) - - return stepPanel - } - - private fun fixExamplesPanel(): JPanel { - val fixes = issue.additionalData.exampleCommitFixes - val examplesCount = fixes.size.coerceAtMost(3) - - val panel = JPanel() - panel.layout = GridLayoutManager(3, 1, JBUI.emptyInsets(), -1, 5) - - panel.add( - defaultFontLabel("External example fixes", true), - baseGridConstraintsAnchorWest(0) - ) - - val labelText = - if (examplesCount == 0) { - "No example fixes available." - } else { - "This issue was fixed by ${issue.additionalData.repoDatasetSize} projects. Here are $examplesCount example fixes." - } - panel.add( - defaultFontLabel(labelText), - baseGridConstraintsAnchorWest(1) - ) - - if (examplesCount == 0) return panel - - val tabbedPane = JBTabbedPane() - // tabbedPane.tabLayoutPolicy = JTabbedPane.SCROLL_TAB_LAYOUT // tabs in one row - tabbedPane.tabComponentInsets = JBInsets.create(0, 0) // no inner borders for tab content - tabbedPane.font = io.snyk.plugin.ui.getFont(-1, 14, tabbedPane.font) - - val tabbedPanel = JPanel() - tabbedPanel.layout = GridLayoutManager(1, 1, JBUI.insetsTop(10), -1, -1) - tabbedPanel.add(tabbedPane, panelGridConstraints(0)) - - panel.add(tabbedPanel, panelGridConstraints(2, indent = 1)) - - val maxRowCount = fixes.take(examplesCount).maxOfOrNull { it.lines.size } ?: 0 - fixes.take(examplesCount).forEach { exampleCommitFix -> - val shortURL = exampleCommitFix.commitURL - .removePrefix("https://") - .replace(Regex("/commit/.*"), "") - - val tabTitle = shortURL.removePrefix("github.com/").let { - if (it.length > 50) it.take(50) + "..." else it - } - - val icon = if (shortURL.startsWith("github.com")) AllIcons.Vcs.Vendors.Github else null - - tabbedPane.addTab( - tabTitle, - icon, - diffPanel(exampleCommitFix, maxRowCount), - shortURL - ) - } - - return panel + private fun overviewPanel(): JComponent? { + return SnykCodeOverviewPanel(issue.additionalData) } - private fun diffPanel(exampleCommitFix: snyk.common.lsp.ExampleCommitFix, rowCount: Int): JComponent { - fun shift(colorComponent: Int, d: Double): Int { - val n = (colorComponent * d).toInt() - return n.coerceIn(0, 255) - } - - val baseColor = UIUtil.getTextFieldBackground() - val addedColor = Color( - shift(baseColor.red, 0.75), - baseColor.green, - shift(baseColor.blue, 0.75) - ) - val removedColor = Color( - shift(baseColor.red, 1.25), - shift(baseColor.green, 0.85), - shift(baseColor.blue, 0.85) - ) - - val panel = JPanel() - panel.layout = GridLayoutManager(rowCount, 1, JBUI.emptyInsets(), -1, 0) - panel.background = baseColor - - exampleCommitFix.lines.forEachIndexed { index, exampleLine -> - val lineText = "%6d %c %s".format( - exampleLine.lineNumber, - when (exampleLine.lineChange) { - "added" -> '+' - "removed" -> '-' - "none" -> ' ' - else -> '!' - }, - exampleLine.line - ) - val codeLine = JTextArea(lineText) - - codeLine.background = when (exampleLine.lineChange) { - "added" -> addedColor - "removed" -> removedColor - "none" -> baseColor - else -> baseColor - } - codeLine.isOpaque = true - codeLine.isEditable = false - codeLine.font = io.snyk.plugin.ui.getFont(-1, 14, codeLine.font) - - panel.add( - codeLine, - baseGridConstraintsAnchorWest( - row = index, - fill = GridConstraints.FILL_BOTH, - indent = 0 - ) - ) - } - - // fill space with empty lines to avoid rows stretching - for (i in exampleCommitFix.lines.size..rowCount) { - val emptyLine = JTextArea("") - panel.add( - emptyLine, - baseGridConstraintsAnchorWest( - row = i - 1, - fill = GridConstraints.FILL_BOTH, - indent = 0 - ) - ) - } - - return ScrollPaneFactory.createScrollPane( - panel, - ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED - ) + private fun dataFlowPanel(): JPanel? { + return SnykCodeDataflowPanel(project, issue.additionalData) } - private fun getOverviewText(): String { - return issue.additionalData.message + private fun fixExamplesPanel(): JPanel? { + return SnykCodeExampleFixesPanel(issue.additionalData) } } @@ -399,3 +97,11 @@ private fun getIssueTitle(issue: ScanIssue) = } else { issue.additionalData.message.split(".").firstOrNull() ?: "Unknown issue" } + +fun defaultFontLabel(labelText: String, bold: Boolean = false): JLabel { + return JLabel().apply { + val titleLabelFont: Font? = io.snyk.plugin.ui.getFont(if (bold) Font.BOLD else -1, 14, font) + titleLabelFont?.let { font = it } + text = labelText + } +} diff --git a/src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt b/src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt new file mode 100644 index 000000000..78d7a6266 --- /dev/null +++ b/src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt @@ -0,0 +1,101 @@ +package snyk.code.annotator + +import com.intellij.codeInsight.intention.PriorityAction +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import icons.SnykIcons +import org.eclipse.lsp4j.CodeAction +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.TextEdit +import snyk.common.ProductType +import snyk.common.intentionactions.SnykIntentionActionBase +import snyk.common.lsp.DocumentChanger +import snyk.common.lsp.LanguageServerWrapper +import snyk.common.lsp.ScanIssue +import java.util.concurrent.TimeUnit +import javax.swing.Icon + +private const val TIMEOUT = 120L + +class CodeActionIntention( + private val issue: ScanIssue, + private val codeAction: CodeAction, + private val product: ProductType, +) : SnykIntentionActionBase() { + private var changes: Map>? = null + + override fun getText(): String = codeAction.title + + override fun invoke(project: Project, editor: Editor?, psiFile: PsiFile?) { + val task = object : Task.Backgroundable(project, this.title()) { + override fun run(p0: ProgressIndicator) { + val languageServer = LanguageServerWrapper.getInstance().languageServer + var resolvedCodeAction = codeAction + if (codeAction.command == null && codeAction.edit == null) { + resolvedCodeAction = + languageServer.textDocumentService + .resolveCodeAction(codeAction).get(TIMEOUT, TimeUnit.SECONDS) + + val edit = resolvedCodeAction.edit + if (edit == null || edit.changes == null) return + changes = edit.changes + } else { + val codeActionCommand = resolvedCodeAction.command + val executeCommandParams = + ExecuteCommandParams(codeActionCommand.command, codeActionCommand.arguments) + languageServer.workspaceService + .executeCommand(executeCommandParams).get(TIMEOUT, TimeUnit.SECONDS) + } + } + + override fun onSuccess() { + // see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360005049419-Run-WriteAction-in-Background-process-Asynchronously + // save the write action to call it later onSuccess + // as we are updating documents, we need a WriteCommandAction + WriteCommandAction.runWriteCommandAction(project) { + if (changes == null) return@runWriteCommandAction + for (change in changes!!) { + DocumentChanger.applyChange(change) + } + } + } + } + ProgressManager.getInstance().run(task) + } + + + override fun getIcon(p0: Int): Icon { + return when (product) { + ProductType.OSS -> SnykIcons.OPEN_SOURCE_SECURITY + ProductType.IAC -> SnykIcons.IAC + ProductType.CONTAINER -> SnykIcons.CONTAINER + ProductType.CODE_SECURITY -> SnykIcons.SNYK_CODE + ProductType.CODE_QUALITY -> SnykIcons.SNYK_CODE + ProductType.ADVISOR -> TODO() + + } + } + + override fun getPriority(): PriorityAction.Priority { + return when { + codeAction.title.contains("fix", ignoreCase = true) -> PriorityAction.Priority.TOP + else -> issue.getSeverityAsEnum().getQuickFixPriority() + } + } + + fun title(): String { + return when (product) { + ProductType.OSS -> "Applying Snyk OpenSource Action" + ProductType.IAC -> "Applying Snyk Infrastructure as Code Action" + ProductType.CONTAINER -> "Applying Snyk Container Action" + ProductType.CODE_SECURITY -> "Applying Snyk Code Action" + ProductType.CODE_QUALITY -> "Applying Snyk Code Action" + ProductType.ADVISOR -> TODO() + } + } +} diff --git a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt b/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt new file mode 100644 index 000000000..3f88f90df --- /dev/null +++ b/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt @@ -0,0 +1,17 @@ +package snyk.code.annotator + +import io.snyk.plugin.Severity +import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel +import snyk.common.intentionactions.ShowDetailsIntentionActionBase +import snyk.common.lsp.ScanIssue + +class ShowDetailsIntentionAction( + override val annotationMessage: String, + private val issue: ScanIssue +) : ShowDetailsIntentionActionBase() { + override fun selectNodeAndDisplayDescription(toolWindowPanel: SnykToolWindowPanel) { + toolWindowPanel.selectNodeAndDisplayDescription(issue) + } + + override fun getSeverity(): Severity = issue.getSeverityAsEnum() +} diff --git a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt b/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt index 1f377d0cf..7ec8a1781 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt +++ b/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotatorLS.kt @@ -1,42 +1,24 @@ package snyk.code.annotator -import com.intellij.codeInsight.intention.PriorityAction import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.ExternalAnnotator -import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task.Backgroundable -import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import icons.SnykIcons -import io.snyk.plugin.Severity import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.isSnykCodeLSEnabled import io.snyk.plugin.isSnykCodeRunning import io.snyk.plugin.toLanguageServerURL -import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel -import org.eclipse.lsp4j.CodeAction import org.eclipse.lsp4j.CodeActionContext import org.eclipse.lsp4j.CodeActionParams -import org.eclipse.lsp4j.ExecuteCommandParams import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.TextDocumentIdentifier -import org.eclipse.lsp4j.TextEdit import snyk.common.AnnotatorCommon -import snyk.common.intentionactions.ShowDetailsIntentionActionBase -import snyk.common.intentionactions.SnykIntentionActionBase -import snyk.common.lsp.DocumentChanger +import snyk.common.ProductType import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException -import javax.swing.Icon - -private const val TIMEOUT = 120L private const val CODEACTION_TIMEOUT = 2L @@ -100,7 +82,7 @@ class SnykCodeAnnotatorLS : ExternalAnnotator() { val title = codeAction.title holder.newAnnotation(highlightSeverity, title) .range(textRange) - .withFix(CodeActionIntention(issue, codeAction)) + .withFix(CodeActionIntention(issue, codeAction, ProductType.CODE_SECURITY)) .create() } } @@ -157,68 +139,4 @@ class SnykCodeAnnotatorLS : ExternalAnnotator() { return TextRange.EMPTY_RANGE } } - - inner class ShowDetailsIntentionAction( - override val annotationMessage: String, - private val issue: ScanIssue - ) : ShowDetailsIntentionActionBase() { - override fun selectNodeAndDisplayDescription(toolWindowPanel: SnykToolWindowPanel) { - toolWindowPanel.selectNodeAndDisplayDescription(issue) - } - - override fun getSeverity(): Severity = issue.getSeverityAsEnum() - } - - inner class CodeActionIntention(private val issue: ScanIssue, private val codeAction: CodeAction) : - SnykIntentionActionBase() { - private var changes: Map>? = null - - override fun getText(): String = codeAction.title - - override fun invoke(project: Project, editor: Editor?, psiFile: PsiFile?) { - val task = object : Backgroundable(project, "Applying Snyk Code Action") { - override fun run(p0: ProgressIndicator) { - val languageServer = LanguageServerWrapper.getInstance().languageServer - var resolvedCodeAction = codeAction - if (codeAction.command == null && codeAction.edit == null) { - resolvedCodeAction = - languageServer.textDocumentService - .resolveCodeAction(codeAction).get(TIMEOUT, TimeUnit.SECONDS) - - val edit = resolvedCodeAction.edit - if (edit == null || edit.changes == null) return - changes = edit.changes - } else { - val codeActionCommand = resolvedCodeAction.command - val executeCommandParams = - ExecuteCommandParams(codeActionCommand.command, codeActionCommand.arguments) - languageServer.workspaceService - .executeCommand(executeCommandParams).get(TIMEOUT, TimeUnit.SECONDS) - } - } - - override fun onSuccess() { - // see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360005049419-Run-WriteAction-in-Background-process-Asynchronously - // save the write action to call it later onSuccess - // as we are updating documents, we need a WriteCommandAction - WriteCommandAction.runWriteCommandAction(project) { - if (changes == null) return@runWriteCommandAction - for (change in changes!!) { - DocumentChanger.applyChange(change) - } - } - } - } - ProgressManager.getInstance().run(task) - } - - override fun getIcon(p0: Int): Icon = SnykIcons.SNYK_CODE - - override fun getPriority(): PriorityAction.Priority { - return when { - codeAction.title.contains("fix", ignoreCase = true) -> PriorityAction.Priority.TOP - else -> issue.getSeverityAsEnum().getQuickFixPriority() - } - } - } }