Skip to content

Commit

Permalink
feat(lint): add lint inspection for sketch patches
Browse files Browse the repository at this point in the history
This commit introduces a new lint inspection feature for sketch patches. It includes the `SketchCodeInspection` and `SketchInspectionError` classes to handle and display lint errors. Additionally, the `SingleFileDiffView` has been updated to perform lint checks on the new code after applying a patch and display any errors found.
  • Loading branch information
phodal committed Feb 16, 2025
1 parent 71a3918 commit db9b3a6
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.phodal.shirecore.sketch.lint

import com.intellij.analysis.AnalysisScope
import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator
import com.intellij.codeInspection.InspectionEngine
import com.intellij.codeInspection.InspectionManager
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ex.GlobalInspectionContextBase
import com.intellij.codeInspection.ex.LocalInspectionToolWrapper
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.profile.codeInspection.InspectionProjectProfileManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.table.JBTable
import com.intellij.util.PairProcessor
import com.phodal.shirecore.ShireCoreBundle
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.BorderFactory
import javax.swing.JPanel
import javax.swing.JTable
import javax.swing.table.DefaultTableModel

object SketchCodeInspection {
fun showErrors(errors: List<SketchInspectionError>, panel: JPanel) {
val columnNames = arrayOf("Line", "Description", "Highlight Type")
val data = errors.map {
arrayOf(it.lineNumber, it.description, it.highlightType.toString())
}.toTypedArray()

val tableModel = DefaultTableModel(data, columnNames)

val table = JBTable(tableModel).apply {
autoResizeMode = JTable.AUTO_RESIZE_ALL_COLUMNS
}

val scrollPane = JBScrollPane(table).apply {
preferredSize = Dimension(480, 400)
}

val errorLabel = JBLabel(ShireCoreBundle.message("sketch.lint.error", errors.size)).apply {
border = BorderFactory.createEmptyBorder(5, 5, 5, 5)
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
createPopup(scrollPane, table, errors).showInCenterOf(panel)
}

override fun mouseEntered(e: MouseEvent) {
toolTipText = ShireCoreBundle.message("sketch.lint.error.tooltip")
}
})
}

val errorPanel = JPanel().apply {
background = JBColor.WHITE
layout = FlowLayout(FlowLayout.LEFT)
add(errorLabel)
}

panel.add(errorPanel)
}

private fun createPopup(
scrollPane: JBScrollPane,
table: JBTable,
errors: List<SketchInspectionError>
): JBPopup = JBPopupFactory.getInstance()
.createComponentPopupBuilder(scrollPane, table)
.setTitle("Found Lint Issues: ${errors.size}")
.setResizable(true)
.setMovable(true)
.setRequestFocus(true)
.createPopup()

fun runInspections(project: Project, psiFile: PsiFile, originFile: VirtualFile): List<SketchInspectionError> {
val globalContext = InspectionManager.getInstance(project).createNewGlobalContext()
as? GlobalInspectionContextBase ?: return emptyList()

val originPsi = runReadAction { PsiManager.getInstance(project).findFile(originFile) }
?: return emptyList()

globalContext.currentScope = AnalysisScope(originPsi)

val toolsCopy = collectTools(project, psiFile, globalContext)
if (toolsCopy.isEmpty()) {
return emptyList()
}

return runReadAction {
val indicator = DaemonProgressIndicator()
val result: Map<LocalInspectionToolWrapper, List<ProblemDescriptor>> = InspectionEngine.inspectEx(
toolsCopy, psiFile, psiFile.textRange, psiFile.textRange, false, false, true,
indicator, PairProcessor.alwaysTrue<LocalInspectionToolWrapper?, ProblemDescriptor?>()
)

val problems = result.values.flatten()
return@runReadAction problems
.sortedBy { it.lineNumber }
.distinctBy { it.lineNumber }.map {
SketchInspectionError.Companion.from(it)
}
}
}

private fun collectTools(
project: Project,
psiFile: PsiFile,
globalContext: GlobalInspectionContextBase
): MutableList<LocalInspectionToolWrapper> {
val inspectionProfile = InspectionProjectProfileManager.getInstance(project).currentProfile
val toolWrappers = inspectionProfile.getInspectionTools(psiFile)
.filter {
it.isApplicable(psiFile.language) && it.defaultLevel.severity == HighlightSeverity.ERROR
}

toolWrappers.forEach {
it.initialize(globalContext)
}

val toolsCopy: MutableList<LocalInspectionToolWrapper> =
ArrayList<LocalInspectionToolWrapper>(toolWrappers.size)
for (tool in toolWrappers) {
if (tool is LocalInspectionToolWrapper) {
toolsCopy.add(tool.createCopy())
}
}

return toolsCopy
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.phodal.shirecore.sketch.lint

import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType

data class SketchInspectionError(
val lineNumber: Int,
val description: String,
val highlightType: ProblemHighlightType,
) {
companion object {
fun from(problemDescriptor: ProblemDescriptor): SketchInspectionError {
return SketchInspectionError(
problemDescriptor.lineNumber,
problemDescriptor.descriptionTemplate,
problemDescriptor.highlightType
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import com.intellij.diff.editor.DiffEditorTabFilesManager
import com.intellij.diff.requests.SimpleDiffRequest
import com.intellij.icons.AllIcons
import com.intellij.lang.Language
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.diff.impl.patch.TextFilePatch
import com.intellij.openapi.diff.impl.patch.apply.GenericPatchApplier
Expand All @@ -18,15 +20,19 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.readText
import com.intellij.psi.PsiManager
import com.intellij.testFramework.LightVirtualFile
import com.intellij.ui.DarculaColors
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.LocalTimeCounter
import com.phodal.shirecore.ShireCoreBundle
import com.phodal.shirecore.sketch.LangSketch
import com.phodal.shirecore.sketch.lint.SketchCodeInspection
import java.awt.BorderLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
Expand All @@ -38,27 +44,30 @@ import javax.swing.JPanel

class SingleFileDiffView(
private val myProject: Project,
private val virtualFile: VirtualFile,
private val currentFile: VirtualFile,
val patch: TextFilePatch,
) : LangSketch {
private val mainPanel: JPanel = JPanel(VerticalLayout(5))
private val myHeaderPanel: JPanel = JPanel(BorderLayout())
private var filePanel: DialogPanel? = null
var diffFile: ChainDiffVirtualFile? = null
private val oldCode = currentFile.readText()
private val appliedPatch = GenericPatchApplier.apply(oldCode, patch.hunks)
private val newCode = appliedPatch?.patchedText ?: ""

init {
val contentPanel = JPanel(BorderLayout())
val actions = createActionButtons()
val filepathLabel = JBLabel(virtualFile.name).apply {
icon = virtualFile.fileType.icon
val filepathLabel = JBLabel(currentFile.name).apply {
icon = currentFile.fileType.icon
border = BorderFactory.createEmptyBorder(2, 10, 2, 10)

addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
val isShowDiffSuccess = showDiff()
if (isShowDiffSuccess) return

FileEditorManager.getInstance(myProject).openFile(virtualFile, true)
FileEditorManager.getInstance(myProject).openFile(currentFile, true)
}

override fun mouseEntered(e: MouseEvent) {
Expand Down Expand Up @@ -94,6 +103,20 @@ class SingleFileDiffView(

mainPanel.add(myHeaderPanel)
mainPanel.add(contentPanel)

ApplicationManager.getApplication().executeOnPooledThread {
lintCheckForNewCode(currentFile)
}
}

fun lintCheckForNewCode(currentFile: VirtualFile) {
if (newCode.isEmpty()) return
val newFile = LightVirtualFile(currentFile, newCode, LocalTimeCounter.currentTime())
val psiFile = runReadAction { PsiManager.getInstance(myProject).findFile(newFile) } ?: return
val errors = SketchCodeInspection.runInspections(myProject, psiFile, currentFile)
if (errors.isNotEmpty()) {
SketchCodeInspection.showErrors(errors, this@SingleFileDiffView.mainPanel)
}
}

private fun showDiff(): Boolean {
Expand All @@ -102,13 +125,13 @@ class SingleFileDiffView(
return true
}

val document = FileDocumentManager.getInstance().getDocument(virtualFile) ?: return false
val document = FileDocumentManager.getInstance().getDocument(currentFile) ?: return false
val appliedPatch = GenericPatchApplier.apply(document.text, patch.hunks)
?: return false

val newText = appliedPatch.patchedText
val diffFactory = DiffContentFactoryEx.getInstanceEx()
val currentDocContent = diffFactory.create(myProject, virtualFile)
val currentDocContent = diffFactory.create(myProject, currentFile)
val newDocContent = diffFactory.create(newText)

val diffRequest =
Expand All @@ -120,7 +143,7 @@ class SingleFileDiffView(
"AI generated"
)

val producer = SimpleDiffRequestProducer.create(virtualFile.path) {
val producer = SimpleDiffRequestProducer.create(currentFile.path) {
diffRequest
}

Expand All @@ -141,7 +164,7 @@ class SingleFileDiffView(

private fun createActionButtons(): List<JButton> {
val undoManager = UndoManager.getInstance(myProject)
val fileEditor = FileEditorManager.getInstance(myProject).getSelectedEditor(virtualFile)
val fileEditor = FileEditorManager.getInstance(myProject).getSelectedEditor(currentFile)

val rollback = JButton(AllIcons.Actions.Rollback).apply {
toolTipText = ShireCoreBundle.message("sketch.patch.action.rollback.tooltip")
Expand All @@ -162,7 +185,7 @@ class SingleFileDiffView(
return listOf(rollback)
}

override fun getViewText(): String = virtualFile.readText()
override fun getViewText(): String = currentFile.readText()

override fun updateViewText(text: String) {}

Expand Down
4 changes: 3 additions & 1 deletion core/src/main/resources/messages/ShireCoreBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ sketch.patch.action.viewDiff.tooltip=View the diff
sketch.patch.action.rollback=Rollback
sketch.patch.action.rollback.tooltip=Rollback the change
chat.panel.send=Send
chat.input.empty.tips=Input cannot be empty
chat.input.empty.tips=Input cannot be empty
sketch.lint.error.tooltip=Click to view all errors
sketch.lint.error=Found Lint issue: {0}

0 comments on commit db9b3a6

Please sign in to comment.