Skip to content

Commit

Permalink
refactor: snyk code ls common functionality [IDE-283] (#528)
Browse files Browse the repository at this point in the history
* refactor: extract common SnykCode annotator functionality

* refactor: extract SnykCode panels from Suggestion Panel

* chore: CHANGELOG
  • Loading branch information
teodora-sandu authored May 13, 2024
1 parent ab094ed commit 1915ccd
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 395 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DataFlow>): 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<JPanel>()
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>
): 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<hyperlink>$linkText</hyperlink>$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
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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("<html>" + codeIssueData.message + "</html>").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))

}
}
Loading

0 comments on commit 1915ccd

Please sign in to comment.