From 2040e518c4de4a7a2878497a23cddb622df67daf Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 5 Jul 2025 16:19:02 +0100 Subject: [PATCH 01/13] Fix: Add/fix some FlowStrings. --- .../mixin/expression/gui/FlowStrings.kt | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt index 01d66ef56..21a564496 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt @@ -39,6 +39,7 @@ import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.FieldInsnNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.TypeInsnNode import org.objectweb.asm.tree.VarInsnNode fun FlowValue.shortString(project: Project, clazz: ClassNode, method: MethodNode): String { @@ -63,6 +64,9 @@ fun FlowValue.shortString(project: Project, clazz: ClassNode, method: MethodNode getDecoration(FlowDecorations.COMPLEX_COMPARISON_JUMP)?.let { jump -> return complexCmpString(insn.opcode, jump.insn.opcode) } + if (insn.opcode == Opcodes.INSTANCEOF) { + return instanceofString(insn as TypeInsnNode) + } return when (val insn = insn) { is FieldInsnNode -> fieldString(insn) is VarInsnNode -> varString(this, insn.`var`, project, clazz, method) @@ -96,6 +100,7 @@ private fun constantString(cst: Any): String { } return when (cst) { Type.VOID_TYPE -> "null" + is Type -> "${cst.shortName}.class" is String -> "'${cst.escape()}'" is Float -> "${cst}F" is Long -> "${cst}L" @@ -128,7 +133,9 @@ private fun varString(flow: FlowValue, index: Int, project: Project, clazz: Clas location = location.next } val localName = ReadAction.compute<_, Nothing> { - LocalVariables.getLocalVariableAt(project, clazz, method, location, index) + runCatching { + LocalVariables.getLocalVariableAt(project, clazz, method, location, index) + }.getOrNull() }?.name ?: "" val isStore = flow.insn.opcode in Opcodes.ISTORE..Opcodes.ASTORE return localName + if (isStore) " =" else "" @@ -154,10 +161,19 @@ private fun castString(type: Type): String = "(${type.shortName})" private fun complexCmpString(opcode: Int, jumpOpcode: Int): String { val isG = opcode == Opcodes.FCMPG || opcode == Opcodes.DCMPG + val isLong = opcode == Opcodes.LCMP return when (jumpOpcode) { Opcodes.IFEQ, Opcodes.IFNE -> "== or !=" - Opcodes.IFLT, Opcodes.IFGE -> if (isG) "<" else ">=" - Opcodes.IFGT, Opcodes.IFLE -> if (isG) "<=" else ">" + Opcodes.IFLT, Opcodes.IFGE -> when { + isLong -> "< or >=" + isG -> "<" + else -> ">=" + } + Opcodes.IFGT, Opcodes.IFLE -> when { + isLong -> "<= or >" + isG -> "<=" + else -> ">" + } else -> "Unknown jump" } } @@ -176,6 +192,11 @@ private fun newArrayString(flow: FlowValue): String? { return "$prefix{ }" } +private fun instanceofString(insn: TypeInsnNode): String { + val type = Type.getObjectType(insn.desc) + return "instanceof ${type.shortName}" +} + private fun opcodeString(opcode: Int) = when (opcode) { Opcodes.ATHROW -> "throw" in Opcodes.IRETURN..Opcodes.RETURN -> "return" From c6d203db50a11351dc2f85136d1baeb640123912 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 5 Jul 2025 20:43:25 +0100 Subject: [PATCH 02/13] New: Show expression matching in flow diagrams. --- .../mixin/expression/MEExpressionMatchUtil.kt | 10 ++ .../mixin/expression/gui/FlowDiagram.kt | 143 ++++++++++++++++-- .../mixin/expression/gui/FlowGraph.kt | 97 +++++++++++- .../expression/gui/MEFlowWindowService.kt | 11 +- .../mixin/expression/gui/MEShowFlowAction.kt | 48 +++++- 5 files changed, 275 insertions(+), 34 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt index f998c5db1..dda21984f 100644 --- a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt @@ -263,6 +263,8 @@ object MEExpressionMatchUtil { insns: Iterable, contextType: ExpressionContext.Type, forCompletion: Boolean, + crossinline reportMatchStatus: (FlowValue, Boolean) -> Unit = { _, _ -> }, + crossinline reportPartialMatch: (FlowValue) -> Unit = {}, callback: (ExpressionMatch) -> Unit ) { for (insn in insns) { @@ -283,6 +285,14 @@ object MEExpressionMatchUtil { // Our maps are per-injector anyway, so this is just a normal decoration. decorations.getOrPut(VirtualInsn(insn), ::mutableMapOf)[key] = value } + + override fun reportMatchStatus(node: FlowValue, expr: Expression, matched: Boolean) { + reportMatchStatus.invoke(node, matched) + } + + override fun reportPartialMatch(node: FlowValue, expr: Expression) { + reportPartialMatch.invoke(node) + } } val flow = flows[insn] ?: continue diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 5764b9d6e..4b713d8a3 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -20,13 +20,20 @@ package com.demonwav.mcdev.platform.mixin.expression.gui +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.colors.EditorFontType +import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project +import com.intellij.psi.PsiModifierList +import com.intellij.ui.JBColor import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil +import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import com.mxgraph.layout.hierarchical.mxHierarchicalLayout import com.mxgraph.model.mxCell import com.mxgraph.swing.mxGraphComponent @@ -38,6 +45,8 @@ import java.awt.BorderLayout import java.awt.Color import java.awt.Dimension import java.awt.Rectangle +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import java.util.SortedMap import javax.swing.JButton import javax.swing.JLabel @@ -56,8 +65,15 @@ private const val INTER_GROUP_SPACING = 75 private const val INTRA_GROUP_SPACING = 75 private const val LINE_NUMBER_STYLE = "LINE_NUMBER" private const val HIGHLIGHT_STYLE = "HIGHLIGHT" +private const val IGNORED_STYLE = "IGNORED" +private const val FAILED_STYLE = "FAILED" +private const val PARTIAL_STYLE = "PARTIAL" +private const val SUCCESS_STYLE = "SUCCESS" class FlowDiagram( + private val comp: mxGraphComponent, + private val flowGraph: FlowGraph, + private val clazz: ClassNode, val method: MethodNode, val panel: JPanel, val scrollToLine: (Int) -> Unit, @@ -65,12 +81,41 @@ class FlowDiagram( companion object { suspend fun create(project: Project, clazz: ClassNode, method: MethodNode): FlowDiagram? { val flowGraph = FlowGraph.parse(project, clazz, method) ?: return null - return buildPanel(flowGraph, method) + return buildDiagram(flowGraph, clazz, method) } } + + fun populateMatchStatuses(module: Module, expression: Expression, modifierList: PsiModifierList) { + flowGraph.resetMatches() + ReadAction.run { + val pool = MEExpressionMatchUtil.createIdentifierPoolFactory(module, clazz, modifierList)(method) + for ((virtualInsn, root) in flowGraph.flowMap) { + val node = flowGraph.allNodes.getValue(root) + MEExpressionMatchUtil.findMatchingInstructions( + clazz, method, pool, flowGraph.flowMap, expression, listOf(virtualInsn), + ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // most permissive + false, + node::reportMatchStatus, + node::reportPartialMatch + ) {} + } + } + showBestNode() + } + + private fun showBestNode() { + val bestNode = flowGraph.allNodes.values.maxBy { it.matchScore } + val bestCell = comp.graph.getChildVertices(comp.graph.defaultParent).asSequence() + .map { it as mxCell } + .find { it.value === bestNode } + ?: return + flowGraph.highlightMatches(bestNode, false) + comp.scrollCellToVisible(bestCell, true) + comp.refresh() + } } -private suspend fun buildPanel(flowGraph: FlowGraph, method: MethodNode): FlowDiagram { +private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: MethodNode): FlowDiagram { val graph = MxFlowGraph() setupStyles(graph) val groupedCells = addGraphContent(graph, flowGraph) @@ -78,19 +123,20 @@ private suspend fun buildPanel(flowGraph: FlowGraph, method: MethodNode): FlowDi val calculateBounds = layOutGraph(graph, groupedCells, lineNumberNodes) val panel: JPanel - val scrollToLine = withContext(Dispatchers.EDT) { + val (comp, scrollToLine) = withContext(Dispatchers.EDT) { panel = JPanel(BorderLayout()) - displayGraphComponent(graph, panel, calculateBounds, lineNumberNodes) + displayGraphComponent(flowGraph, graph, panel, calculateBounds, lineNumberNodes) } - return FlowDiagram(method, panel, scrollToLine) + return FlowDiagram(comp, flowGraph, clazz, method, panel, scrollToLine) } private fun displayGraphComponent( + flowGraph: FlowGraph, graph: mxGraph, panel: JPanel, calculateBounds: () -> Dimension, lineNumberNodes: SortedMap -): (Int) -> Unit { +): Pair Unit> { val comp = mxGraphComponent(graph) fun fixBounds() { comp.graphControl.preferredSize = calculateBounds() @@ -100,13 +146,13 @@ private fun displayGraphComponent( fixBounds() } fixBounds() - configureGraphComponent(comp) + configureGraphComponent(comp, flowGraph) val toolbar = createToolbar(comp, ::fixBounds) panel.add(toolbar, BorderLayout.NORTH) panel.add(comp, BorderLayout.CENTER) - return { lineNumber -> + return comp to { lineNumber -> lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) -> scrollCellToVisible(comp, node) } @@ -164,17 +210,14 @@ private fun createSearchField(comp: mxGraphComponent, fixBounds: () -> Unit): JT var scrolled = false for (cell in vertices) { - cell as mxCell - if (cell.style == LINE_NUMBER_STYLE) { - continue - } + val flow = (cell as mxCell).value as? FlowNode ?: continue val texts = listOf( graph.convertValueToString(cell), graph.getToolTipForCell(cell), ) if (searchText.isNotEmpty() && texts.any { searchText in it.lowercase() }) { - graph.setCellStyle(HIGHLIGHT_STYLE, arrayOf(cell)) + flow.searchHighlight = true if (!scrolled) { comp.scrollCellToVisible(cell, true) comp.zoomTo(1.2, true) @@ -182,7 +225,7 @@ private fun createSearchField(comp: mxGraphComponent, fixBounds: () -> Unit): JT scrolled = true } } else { - graph.model.setStyle(cell, null) + flow.searchHighlight = false } } } @@ -203,6 +246,24 @@ private class MxFlowGraph : mxGraph() { val flow = (cell as? mxCell)?.value as? FlowNode ?: return super.convertValueToString(cell) return flow.shortText } + + override fun getCellStyle(cell: Any?): MutableMap { + val defaultStyle = super.getCellStyle(cell) + val flow = (cell as? mxCell)?.value as? FlowNode ?: return defaultStyle + val styles = buildList { + when (flow.currentMatchStatus) { + MatchStatus.IGNORED -> add(IGNORED_STYLE) + MatchStatus.FAIL -> add(FAILED_STYLE) + MatchStatus.PARTIAL -> add(PARTIAL_STYLE) + MatchStatus.SUCCESS -> add(SUCCESS_STYLE) + null -> {} + } + if (flow.searchHighlight) { + add(HIGHLIGHT_STYLE) + } + } + return styles.fold(defaultStyle) { acc, style -> stylesheet.getCellStyle(style, acc) } + } } private suspend fun addGraphContent( @@ -316,9 +377,39 @@ private fun setupStyles(graph: mxGraph) { mxConstants.STYLE_STROKEWIDTH to "2", ) ) + graph.stylesheet.putCellStyle( + IGNORED_STYLE, + mapOf( + mxConstants.STYLE_OPACITY to 20, + mxConstants.STYLE_TEXT_OPACITY to 20, + mxConstants.STYLE_STROKE_OPACITY to 20, + mxConstants.STYLE_FILL_OPACITY to 20, + ) + ) + graph.stylesheet.putCellStyle( + FAILED_STYLE, + mapOf( + mxConstants.STYLE_STROKECOLOR to JBColor.red.hexString, + mxConstants.STYLE_STROKEWIDTH to "2", + ) + ) + graph.stylesheet.putCellStyle( + PARTIAL_STYLE, + mapOf( + mxConstants.STYLE_STROKECOLOR to JBColor.orange.hexString, + mxConstants.STYLE_STROKEWIDTH to "2", + ) + ) + graph.stylesheet.putCellStyle( + SUCCESS_STYLE, + mapOf( + mxConstants.STYLE_STROKECOLOR to JBColor.green.hexString, + mxConstants.STYLE_STROKEWIDTH to "2", + ) + ) } -private fun configureGraphComponent(comp: mxGraphComponent) { +private fun configureGraphComponent(comp: mxGraphComponent, flowGraph: FlowGraph) { val graph = comp.graph graph.isCellsSelectable = false graph.isCellsEditable = false @@ -333,6 +424,28 @@ private fun configureGraphComponent(comp: mxGraphComponent) { comp.graphControl.setOpaque(false) comp.verticalScrollBar.setUnitIncrement(16) comp.horizontalScrollBar.setUnitIncrement(16) + + configureMouseListeners(comp, flowGraph) +} + +private fun configureMouseListeners(comp: mxGraphComponent, flowGraph: FlowGraph) { + fun highlight(e: MouseEvent, soft: Boolean) { + val node = (comp.getCellAt(e.x, e.y) as mxCell?)?.value as? FlowNode + flowGraph.highlightMatches(node, soft) + comp.refresh() + e.consume() + } + + comp.graphControl.addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + highlight(e, false) + } + }) + comp.graphControl.addMouseMotionListener(object : MouseAdapter() { + override fun mouseMoved(e: MouseEvent) { + highlight(e, true) + } + }) } private val Color.hexString get() = "#%06X".format(rgb) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt index 26797541e..d9f1c72d4 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.expression.gui +import com.demonwav.mcdev.platform.mixin.expression.FlowMap import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil import com.intellij.openapi.application.readAction import com.intellij.openapi.progress.checkCanceled @@ -31,13 +32,65 @@ import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.LineNumberNode import org.objectweb.asm.tree.MethodNode -class FlowNode(val flow: FlowValue, project: Project, clazz: ClassNode, method: MethodNode) { - val inputs = (0.. +) { + private val matches = mutableMapOf().withDefault { MatchStatus.IGNORED } + var currentMatchStatus: MatchStatus? = null + private set + val inputs = (0..= MatchStatus.PARTIAL } + + init { + map[flow] = this + } fun dfs(): Sequence = sequenceOf(this) + inputs.asSequence().flatMap { it.dfs() } + + fun resetMatches() { + matches.clear() + clearMatchHighlight() + } + + fun clearMatchHighlight() { + currentMatchStatus = null + } + + fun reportMatchStatus(childFlow: FlowValue, matched: Boolean) { + updateMatchStatus(childFlow, if (matched) MatchStatus.SUCCESS else MatchStatus.FAIL) + } + + fun reportPartialMatch(childFlow: FlowValue) { + updateMatchStatus(childFlow, MatchStatus.PARTIAL) + } + + private fun updateMatchStatus(childFlow: FlowValue, status: MatchStatus) { + matches.compute(childFlow) { _, oldStatus -> + if (oldStatus == null) { + status + } else { + maxOf(oldStatus, status) + } + } + } + + fun highlightMatches(allNodes: Iterable) { + for (node in allNodes) { + node.currentMatchStatus = matches.getValue(node.flow) + } + } } class FlowGroup(val root: FlowNode, method: MethodNode) : Comparable { @@ -56,24 +109,54 @@ class FlowGroup(val root: FlowNode, method: MethodNode) : Comparable override fun compareTo(other: FlowGroup) = compareValuesBy(this, other, { it.lineNumber }, { it.startIndex }) } -class FlowGraph(val groups: SortedSet) { +class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allNodes: Map) { + private var highlightRoot: FlowNode? = null + private var hardHighlight = false + operator fun iterator() = groups.iterator() companion object { suspend fun parse(project: Project, clazz: ClassNode, method: MethodNode): FlowGraph? { - val flows = readAction { MEExpressionMatchUtil.getFlowMap(project, clazz, method) }?.values ?: return null + val flows = readAction { MEExpressionMatchUtil.getFlowMap(project, clazz, method) } ?: return null val groups = sortedSetOf() - for (flow in flows) { + val allNodes = mutableMapOf() + for (flow in flows.values) { if (!flow.isRoot) { continue } @Suppress("UnstableApiUsage") checkCanceled() - val node = FlowNode(flow, project, clazz, method) + val node = FlowNode(flow, project, clazz, method, allNodes) groups.add(FlowGroup(node, method)) } - return FlowGraph(groups) + return FlowGraph(groups, flows, allNodes) + } + } + + fun resetMatches() { + highlightRoot = null + for (node in allNodes.values) { + node.resetMatches() + } + } + + fun highlightMatches(root: FlowNode?, soft: Boolean) { + if (hardHighlight && soft) { + return + } + hardHighlight = root != null && !soft + if (root == highlightRoot) { + return + } + highlightRoot = root + clearMatchHighlights() + root?.highlightMatches(allNodes.values) + } + + private fun clearMatchHighlights() { + for (node in allNodes.values) { + node.clearMatchHighlight() } } } diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt index 4fde8c967..c795b4a84 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt @@ -46,13 +46,13 @@ private val FLOW_DIAGRAM_KEY = Key.create("${MEFlowWindowService::c @Service(Service.Level.PROJECT) class MEFlowWindowService(private val project: Project, private val scope: CoroutineScope) { - fun showDiagram(clazz: ClassNode, method: MethodNode, lineNumber: Int?) { + fun showDiagram(clazz: ClassNode, method: MethodNode, action: (FlowDiagram) -> Unit) { scope.launch(Dispatchers.EDT) { - showDiagramImpl(clazz, method, lineNumber) + showDiagramImpl(clazz, method, action) } } - private suspend fun showDiagramImpl(clazz: ClassNode, method: MethodNode, lineNumber: Int?) { + private suspend fun showDiagramImpl(clazz: ClassNode, method: MethodNode, action: (FlowDiagram) -> Unit) { val toolWindowManager = ToolWindowManager.getInstance(project) var toolWindow = toolWindowManager.getToolWindow(TOOL_WINDOW_ID) @@ -68,10 +68,9 @@ class MEFlowWindowService(private val project: Project, private val scope: Corou project, "Failed to create flow diagram", "Error" ) toolWindow.contentManager.setSelectedContent(content) - if (lineNumber != null) { - content.getUserData(FLOW_DIAGRAM_KEY)?.scrollToLine?.invoke(lineNumber) + toolWindow.activate { + content.getUserData(FLOW_DIAGRAM_KEY)?.let(action) } - toolWindow.activate(null) } private suspend fun chooseContent(toolWindow: ToolWindow, clazz: ClassNode, method: MethodNode): Content? { diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt index 42f66591a..da94c9da5 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt @@ -20,14 +20,23 @@ package com.demonwav.mcdev.platform.mixin.expression.gui +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MethodReference +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.findClassNodeByPsiClass +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.descriptor +import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.lang.java.JavaLanguage import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.components.service import com.intellij.psi.PsiClass import com.intellij.psi.PsiIdentifier @@ -48,11 +57,12 @@ class MEShowFlowAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return - val (clazz, method, lineNumber) = resolve(e) ?: return - project.service().showDiagram(clazz, method, lineNumber) + val (clazz, method, action) = resolve(e) ?: return + project.service().showDiagram(clazz, method, action) } private fun resolve(e: AnActionEvent): Resolved? { + val project = e.project ?: return null val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null if (file.language != JavaLanguage.INSTANCE) { return null @@ -80,18 +90,44 @@ class MEShowFlowAction : AnAction() { fun resolveMethodByLine(): Resolved? { val clazz = findClassNodeByPsiClass(psiClass) ?: return null val lineNumber = caret.logicalPosition.line + 1 - val method = clazz.methods.find { method -> + val method = clazz.methods.lastOrNull { method -> method.instructions.asSequence() .filterIsInstance() .any { it.line == lineNumber } } ?: return null - return Resolved(clazz, method, lineNumber) + return Resolved(clazz, method) { + it.scrollToLine(lineNumber) + } + } + + fun resolveExpressionTarget(): Resolved? { + val module = e.getData(LangDataKeys.MODULE) ?: return null + val string = element.parentOfType() ?: return null + val expression = string.constantStringValue?.let(MEExpressionMatchUtil::createExpression) ?: return null + val modifierList = string.parentOfType()?.modifierList ?: return null + if (InjectedLanguageManager.getInstance(project).getInjectedPsiFiles(string).orEmpty() + .none { it.first is MEExpressionFile } + ) { + return null + } + val (injectorAnnotation, injector) = + modifierList.annotations.firstNotNullOfOrNull { ann -> + (MixinAnnotationHandler.forMixinAnnotation(ann, project) as? InjectorAnnotationHandler) + ?.let { ann to it } + } ?: return null + val targetClass = psiClass.mixinTargets.singleOrNull() ?: return null + val target = injector.resolveTarget(injectorAnnotation, targetClass).singleOrNull() + as? MethodTargetMember ?: return null + return Resolved(target.classAndMethod.clazz, target.classAndMethod.method) { + it.populateMatchStatuses(module, expression, modifierList) + } } - return resolveMixinMethodString() + return resolveExpressionTarget() + ?: resolveMixinMethodString() ?: resolvePsiMethod() ?: resolveMethodByLine() } - private data class Resolved(val clazz: ClassNode, val method: MethodNode, val line: Int? = null) + private data class Resolved(val clazz: ClassNode, val method: MethodNode, val action: (FlowDiagram) -> Unit = {}) } From bec001d3e3d4f5f2193ffa734496d145e28c77ed Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 5 Jul 2025 20:54:29 +0100 Subject: [PATCH 03/13] Cleanup: Extract FlowDiagram styles to DiagramStyles. --- .../mixin/expression/gui/DiagramStyles.kt | 62 ++++++++++ .../mixin/expression/gui/FlowDiagram.kt | 106 +++--------------- 2 files changed, 78 insertions(+), 90 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt diff --git a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt new file mode 100644 index 000000000..0b0f5721e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt @@ -0,0 +1,62 @@ +package com.demonwav.mcdev.platform.mixin.expression.gui + +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.EditorFontType +import com.intellij.ui.JBColor +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import com.mxgraph.util.mxConstants +import java.awt.Color + +object DiagramStyles { + val DEFAULT_NODE + get() = mapOf( + mxConstants.STYLE_FONTFAMILY to EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN).family, + mxConstants.STYLE_ROUNDED to true, + mxConstants.STYLE_FILLCOLOR to JBUI.CurrentTheme.Button.buttonColorStart().hexString, + mxConstants.STYLE_FONTCOLOR to UIUtil.getLabelForeground().hexString, + mxConstants.STYLE_STROKECOLOR to JBUI.CurrentTheme.Button.buttonOutlineColorStart(false).hexString, + mxConstants.STYLE_ALIGN to mxConstants.ALIGN_CENTER, + mxConstants.STYLE_VERTICAL_ALIGN to mxConstants.ALIGN_TOP, + mxConstants.STYLE_SHAPE to mxConstants.SHAPE_LABEL, + mxConstants.STYLE_SPACING to 5, + mxConstants.STYLE_SPACING_TOP to 3, + ) + val DEFAULT_EDGE + get() = mapOf( + mxConstants.STYLE_STROKECOLOR to UIUtil.getFocusedBorderColor().hexString, + ) + val LINE_NUMBER = mapOf( + mxConstants.STYLE_FONTSIZE to "16", + mxConstants.STYLE_STROKECOLOR to "none", + mxConstants.STYLE_FILLCOLOR to "none", + ) + val SEARCH_HIGHLIGHT + get() = mapOf( + mxConstants.STYLE_STROKECOLOR to UIUtil.getFocusedBorderColor().hexString, + mxConstants.STYLE_STROKEWIDTH to "2", + ) + val IGNORED = mapOf( + mxConstants.STYLE_OPACITY to 20, + mxConstants.STYLE_TEXT_OPACITY to 20, + mxConstants.STYLE_STROKE_OPACITY to 20, + mxConstants.STYLE_FILL_OPACITY to 20, + ) + val FAILED + get() = mapOf( + mxConstants.STYLE_STROKECOLOR to JBColor.red.hexString, + mxConstants.STYLE_STROKEWIDTH to "2", + ) + val PARTIAL_MATCH + get() = mapOf( + mxConstants.STYLE_STROKECOLOR to JBColor.orange.hexString, + mxConstants.STYLE_STROKEWIDTH to "2", + ) + val SUCCESS + get() = mapOf( + mxConstants.STYLE_STROKECOLOR to JBColor.green.hexString, + mxConstants.STYLE_STROKEWIDTH to "2", + ) +} + +private val Color.hexString get() = "#%06X".format(rgb) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 4b713d8a3..80106bbb4 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -24,25 +24,19 @@ import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project import com.intellij.psi.PsiModifierList -import com.intellij.ui.JBColor -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import com.mxgraph.layout.hierarchical.mxHierarchicalLayout import com.mxgraph.model.mxCell import com.mxgraph.swing.mxGraphComponent -import com.mxgraph.util.mxConstants import com.mxgraph.util.mxEvent import com.mxgraph.util.mxRectangle import com.mxgraph.view.mxGraph import java.awt.BorderLayout -import java.awt.Color import java.awt.Dimension import java.awt.Rectangle import java.awt.event.MouseAdapter @@ -64,11 +58,6 @@ private const val OUTER_PADDING = 30.0 private const val INTER_GROUP_SPACING = 75 private const val INTRA_GROUP_SPACING = 75 private const val LINE_NUMBER_STYLE = "LINE_NUMBER" -private const val HIGHLIGHT_STYLE = "HIGHLIGHT" -private const val IGNORED_STYLE = "IGNORED" -private const val FAILED_STYLE = "FAILED" -private const val PARTIAL_STYLE = "PARTIAL" -private const val SUCCESS_STYLE = "SUCCESS" class FlowDiagram( private val comp: mxGraphComponent, @@ -248,21 +237,19 @@ private class MxFlowGraph : mxGraph() { } override fun getCellStyle(cell: Any?): MutableMap { - val defaultStyle = super.getCellStyle(cell) - val flow = (cell as? mxCell)?.value as? FlowNode ?: return defaultStyle - val styles = buildList { - when (flow.currentMatchStatus) { - MatchStatus.IGNORED -> add(IGNORED_STYLE) - MatchStatus.FAIL -> add(FAILED_STYLE) - MatchStatus.PARTIAL -> add(PARTIAL_STYLE) - MatchStatus.SUCCESS -> add(SUCCESS_STYLE) - null -> {} - } - if (flow.searchHighlight) { - add(HIGHLIGHT_STYLE) - } + val result = super.getCellStyle(cell).toMutableMap() + val flow = (cell as? mxCell)?.value as? FlowNode ?: return result + when (flow.currentMatchStatus) { + MatchStatus.IGNORED -> result += DiagramStyles.IGNORED + MatchStatus.FAIL -> result += DiagramStyles.FAILED + MatchStatus.PARTIAL -> result += DiagramStyles.PARTIAL_MATCH + MatchStatus.SUCCESS -> result += DiagramStyles.SUCCESS + null -> {} + } + if (flow.searchHighlight) { + result += DiagramStyles.SEARCH_HIGHLIGHT } - return styles.fold(defaultStyle) { acc, style -> stylesheet.getCellStyle(style, acc) } + return result } } @@ -344,69 +331,10 @@ private suspend fun layOutGraph( } private fun setupStyles(graph: mxGraph) { - val colorScheme = EditorColorsManager.getInstance().globalScheme - graph.stylesheet.defaultVertexStyle.let { - it[mxConstants.STYLE_FONTFAMILY] = colorScheme.getFont(EditorFontType.PLAIN).family - it[mxConstants.STYLE_ROUNDED] = true - it[mxConstants.STYLE_FILLCOLOR] = JBUI.CurrentTheme.Button.buttonColorStart().hexString - it[mxConstants.STYLE_FONTCOLOR] = UIUtil.getLabelForeground().hexString - it[mxConstants.STYLE_STROKECOLOR] = JBUI.CurrentTheme.Button.buttonOutlineColorStart(false).hexString - it[mxConstants.STYLE_ALIGN] = mxConstants.ALIGN_CENTER - it[mxConstants.STYLE_VERTICAL_ALIGN] = mxConstants.ALIGN_TOP - it[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_LABEL - it[mxConstants.STYLE_SPACING] = 5 - it[mxConstants.STYLE_SPACING_TOP] = 3 - } - - graph.stylesheet.defaultEdgeStyle.let { - it[mxConstants.STYLE_STROKECOLOR] = UIUtil.getFocusedBorderColor().hexString - } - - graph.stylesheet.putCellStyle( - LINE_NUMBER_STYLE, - mapOf( - mxConstants.STYLE_FONTSIZE to "16", - mxConstants.STYLE_STROKECOLOR to "none", - mxConstants.STYLE_FILLCOLOR to "none", - ) - ) - graph.stylesheet.putCellStyle( - HIGHLIGHT_STYLE, - mapOf( - mxConstants.STYLE_STROKECOLOR to UIUtil.getFocusedBorderColor().hexString, - mxConstants.STYLE_STROKEWIDTH to "2", - ) - ) - graph.stylesheet.putCellStyle( - IGNORED_STYLE, - mapOf( - mxConstants.STYLE_OPACITY to 20, - mxConstants.STYLE_TEXT_OPACITY to 20, - mxConstants.STYLE_STROKE_OPACITY to 20, - mxConstants.STYLE_FILL_OPACITY to 20, - ) - ) - graph.stylesheet.putCellStyle( - FAILED_STYLE, - mapOf( - mxConstants.STYLE_STROKECOLOR to JBColor.red.hexString, - mxConstants.STYLE_STROKEWIDTH to "2", - ) - ) - graph.stylesheet.putCellStyle( - PARTIAL_STYLE, - mapOf( - mxConstants.STYLE_STROKECOLOR to JBColor.orange.hexString, - mxConstants.STYLE_STROKEWIDTH to "2", - ) - ) - graph.stylesheet.putCellStyle( - SUCCESS_STYLE, - mapOf( - mxConstants.STYLE_STROKECOLOR to JBColor.green.hexString, - mxConstants.STYLE_STROKEWIDTH to "2", - ) - ) + val stylesheet = graph.stylesheet + stylesheet.defaultVertexStyle.putAll(DiagramStyles.DEFAULT_NODE) + stylesheet.defaultEdgeStyle.putAll(DiagramStyles.DEFAULT_EDGE) + stylesheet.putCellStyle(LINE_NUMBER_STYLE, DiagramStyles.LINE_NUMBER) } private fun configureGraphComponent(comp: mxGraphComponent, flowGraph: FlowGraph) { @@ -448,8 +376,6 @@ private fun configureMouseListeners(comp: mxGraphComponent, flowGraph: FlowGraph }) } -private val Color.hexString get() = "#%06X".format(rgb) - private inline fun mxGraph.update(routine: () -> T): T { model.beginUpdate() try { From b54abe1bc3cc0936c20fcd5445c5d9a6c09e7071 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 5 Jul 2025 23:20:49 +0100 Subject: [PATCH 04/13] New: Add refreshing to flow graph matching. --- .../mixin/expression/gui/FlowDiagram.kt | 170 +++++++++++++++--- .../mixin/expression/gui/FlowGraph.kt | 13 +- .../mixin/expression/gui/MEShowFlowAction.kt | 5 +- 3 files changed, 158 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 80106bbb4..80190f007 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -21,14 +21,21 @@ package com.demonwav.mcdev.platform.mixin.expression.gui import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil +import com.demonwav.mcdev.util.constantStringValue +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project +import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiModifierList -import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression +import com.intellij.psi.SmartPointerManager +import com.intellij.ui.components.JBLayeredPane +import com.intellij.util.ui.JBUI import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import com.mxgraph.layout.hierarchical.mxHierarchicalLayout import com.mxgraph.model.mxCell @@ -37,16 +44,25 @@ import com.mxgraph.util.mxEvent import com.mxgraph.util.mxRectangle import com.mxgraph.view.mxGraph import java.awt.BorderLayout +import java.awt.Component import java.awt.Dimension +import java.awt.FlowLayout import java.awt.Rectangle +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.util.SortedMap +import java.util.concurrent.Callable +import javax.swing.Icon import javax.swing.JButton +import javax.swing.JComponent import javax.swing.JLabel +import javax.swing.JLayeredPane import javax.swing.JPanel import javax.swing.JTextField import javax.swing.JToolBar +import javax.swing.SwingUtilities import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener import kotlinx.coroutines.Dispatchers @@ -65,7 +81,7 @@ class FlowDiagram( private val clazz: ClassNode, val method: MethodNode, val panel: JPanel, - val scrollToLine: (Int) -> Unit, + val callbacks: FlowDiagramCallbacks, ) { companion object { suspend fun create(project: Project, clazz: ClassNode, method: MethodNode): FlowDiagram? { @@ -74,22 +90,50 @@ class FlowDiagram( } } - fun populateMatchStatuses(module: Module, expression: Expression, modifierList: PsiModifierList) { - flowGraph.resetMatches() - ReadAction.run { - val pool = MEExpressionMatchUtil.createIdentifierPoolFactory(module, clazz, modifierList)(method) - for ((virtualInsn, root) in flowGraph.flowMap) { - val node = flowGraph.allNodes.getValue(root) - MEExpressionMatchUtil.findMatchingInstructions( - clazz, method, pool, flowGraph.flowMap, expression, listOf(virtualInsn), - ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // most permissive - false, - node::reportMatchStatus, - node::reportPartialMatch - ) {} - } + var matchExpression: ((jump: Boolean) -> Unit) = {} + private set + + fun populateMatchStatuses( + module: Module, + currentStringLit: PsiLiteralExpression, + currentModifierList: PsiModifierList + ) { + val stringRef = SmartPointerManager.getInstance(module.project).createSmartPsiElementPointer(currentStringLit) + val modifierListRef = + SmartPointerManager.getInstance(module.project).createSmartPsiElementPointer(currentModifierList) + this.matchExpression = { jump -> + val oldHighlightRoot = flowGraph.highlightRoot + callbacks.setButtonsVisible(false) + flowGraph.resetMatches() + ReadAction.nonBlocking(Callable run@{ + val stringLit = stringRef.element ?: return@run + val modifierList = modifierListRef.element ?: return@run + val expression = stringLit.constantStringValue?.let(MEExpressionMatchUtil::createExpression) + ?: return@run + val pool = MEExpressionMatchUtil.createIdentifierPoolFactory(module, clazz, modifierList)(method) + for ((virtualInsn, root) in flowGraph.flowMap) { + val node = flowGraph.allNodes.getValue(root) + MEExpressionMatchUtil.findMatchingInstructions( + clazz, method, pool, flowGraph.flowMap, expression, listOf(virtualInsn), + ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // most permissive + false, + node::reportMatchStatus, + node::reportPartialMatch + ) {} + } + flowGraph.markHasMatchData() + flowGraph.highlightMatches(oldHighlightRoot, false) + }) + .finishOnUiThread(ModalityState.nonModal()) { + if (jump) { + showBestNode() + } + comp.refresh() + callbacks.setButtonsVisible(true) + } + .submit(ApplicationManager.getApplication()::executeOnPooledThread) } - showBestNode() + matchExpression(true) } private fun showBestNode() { @@ -100,11 +144,28 @@ class FlowDiagram( ?: return flowGraph.highlightMatches(bestNode, false) comp.scrollCellToVisible(bestCell, true) + } + + fun clearExpression() { + callbacks.setButtonsVisible(false) + flowGraph.resetMatches() comp.refresh() } } +class FlowDiagramCallbacks(val scrollToLine: (Int) -> Unit, val setButtonsVisible: (Boolean) -> Unit) + +private class FlowDiagramRef { + lateinit var diagram: FlowDiagram + private set + + fun bind(newDiagram: FlowDiagram) { + diagram = newDiagram + } +} + private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: MethodNode): FlowDiagram { + val diagramRef = FlowDiagramRef() val graph = MxFlowGraph() setupStyles(graph) val groupedCells = addGraphContent(graph, flowGraph) @@ -112,20 +173,21 @@ private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: val calculateBounds = layOutGraph(graph, groupedCells, lineNumberNodes) val panel: JPanel - val (comp, scrollToLine) = withContext(Dispatchers.EDT) { + val (comp, callbacks) = withContext(Dispatchers.EDT) { panel = JPanel(BorderLayout()) - displayGraphComponent(flowGraph, graph, panel, calculateBounds, lineNumberNodes) + displayGraphComponent(diagramRef, flowGraph, graph, panel, calculateBounds, lineNumberNodes) } - return FlowDiagram(comp, flowGraph, clazz, method, panel, scrollToLine) + return FlowDiagram(comp, flowGraph, clazz, method, panel, callbacks).also(diagramRef::bind) } private fun displayGraphComponent( + diagramRef: FlowDiagramRef, flowGraph: FlowGraph, graph: mxGraph, panel: JPanel, calculateBounds: () -> Dimension, lineNumberNodes: SortedMap -): Pair Unit> { +): Pair { val comp = mxGraphComponent(graph) fun fixBounds() { comp.graphControl.preferredSize = calculateBounds() @@ -139,15 +201,71 @@ private fun displayGraphComponent( val toolbar = createToolbar(comp, ::fixBounds) panel.add(toolbar, BorderLayout.NORTH) - panel.add(comp, BorderLayout.CENTER) - return comp to { lineNumber -> - lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) -> - scrollCellToVisible(comp, node) - } + val container = JBLayeredPane().apply { + add(comp, JLayeredPane.DEFAULT_LAYER as Any) + } + + val buttonWrapper = setupFloatingButtons(diagramRef, comp, container) + panel.add(container, BorderLayout.CENTER) + + return comp to FlowDiagramCallbacks( + scrollToLine = { lineNumber -> + lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) -> + scrollCellToVisible(comp, node) + } + }, + setButtonsVisible = { visible -> + buttonWrapper.isVisible = visible + }, + ) +} + +private fun setupFloatingButtons(diagramRef: FlowDiagramRef, comp: mxGraphComponent, container: JBLayeredPane): JComponent { + val refreshButton = makeButton(AllIcons.Actions.Refresh, "Re-match Expression") { + diagramRef.diagram.matchExpression(false) } + + val closeButton = makeButton(AllIcons.Actions.CloseDarkGrey, "Clear Match Data") { + diagramRef.diagram.clearExpression() + } + + val buttonWrapper = JPanel().apply { + isVisible = false + layout = FlowLayout(FlowLayout.RIGHT, 3, 5) + alignmentX = Component.RIGHT_ALIGNMENT + alignmentY = Component.TOP_ALIGNMENT + border = JBUI.Borders.empty(4) + add(refreshButton) + add(closeButton) + } + + container.add(buttonWrapper, JLayeredPane.PALETTE_LAYER as Any) + + container.addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + comp.setBounds(0, 0, container.width, container.height) + + val parentWidth = container.width + val childWidth = buttonWrapper.preferredSize.width + val childHeight = buttonWrapper.preferredSize.height + val margin = 20 + + buttonWrapper.setBounds(parentWidth - childWidth - margin, margin, childWidth, childHeight) + } + }) + return buttonWrapper } +private fun makeButton(icon: Icon, tooltip: String, action: () -> Unit): JButton = + JButton(icon).apply { + toolTipText = tooltip + preferredSize = Dimension(32, 32) + addActionListener { + action() + } + } + private fun scrollCellToVisible(comp: mxGraphComponent, node: mxCell) { // Scrolls the cell to the top of the screen if possible val graph = comp.graph diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt index d9f1c72d4..82c4c718e 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt @@ -110,8 +110,10 @@ class FlowGroup(val root: FlowNode, method: MethodNode) : Comparable } class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allNodes: Map) { - private var highlightRoot: FlowNode? = null + var highlightRoot: FlowNode? = null + private set private var hardHighlight = false + private var hasMatchData = false operator fun iterator() = groups.iterator() @@ -135,13 +137,22 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN } fun resetMatches() { + hasMatchData = false highlightRoot = null + hardHighlight = false for (node in allNodes.values) { node.resetMatches() } } + fun markHasMatchData() { + hasMatchData = true + } + fun highlightMatches(root: FlowNode?, soft: Boolean) { + if (!hasMatchData) { + return + } if (hardHighlight && soft) { return } diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt index da94c9da5..0f82c4e4f 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt @@ -96,14 +96,13 @@ class MEShowFlowAction : AnAction() { .any { it.line == lineNumber } } ?: return null return Resolved(clazz, method) { - it.scrollToLine(lineNumber) + it.callbacks.scrollToLine(lineNumber) } } fun resolveExpressionTarget(): Resolved? { val module = e.getData(LangDataKeys.MODULE) ?: return null val string = element.parentOfType() ?: return null - val expression = string.constantStringValue?.let(MEExpressionMatchUtil::createExpression) ?: return null val modifierList = string.parentOfType()?.modifierList ?: return null if (InjectedLanguageManager.getInstance(project).getInjectedPsiFiles(string).orEmpty() .none { it.first is MEExpressionFile } @@ -119,7 +118,7 @@ class MEShowFlowAction : AnAction() { val target = injector.resolveTarget(injectorAnnotation, targetClass).singleOrNull() as? MethodTargetMember ?: return null return Resolved(target.classAndMethod.clazz, target.classAndMethod.method) { - it.populateMatchStatuses(module, expression, modifierList) + it.populateMatchStatuses(module, string, modifierList) } } From a3c9f49be2b6ab6f408d6169e2b503d06357780e Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sat, 5 Jul 2025 23:39:00 +0100 Subject: [PATCH 05/13] Improvement: Show attempted matches in flow graph tooltips. --- .../mixin/expression/MEExpressionMatchUtil.kt | 8 ++-- .../mixin/expression/gui/FlowDiagram.kt | 34 ++++++++++---- .../mixin/expression/gui/FlowGraph.kt | 47 ++++++++++++++----- 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt index dda21984f..b8f2e0c83 100644 --- a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt @@ -263,8 +263,8 @@ object MEExpressionMatchUtil { insns: Iterable, contextType: ExpressionContext.Type, forCompletion: Boolean, - crossinline reportMatchStatus: (FlowValue, Boolean) -> Unit = { _, _ -> }, - crossinline reportPartialMatch: (FlowValue) -> Unit = {}, + crossinline reportMatchStatus: (FlowValue, Expression, Boolean) -> Unit = { _, _, _ -> }, + crossinline reportPartialMatch: (FlowValue, Expression) -> Unit = { _, _ -> }, callback: (ExpressionMatch) -> Unit ) { for (insn in insns) { @@ -287,11 +287,11 @@ object MEExpressionMatchUtil { } override fun reportMatchStatus(node: FlowValue, expr: Expression, matched: Boolean) { - reportMatchStatus.invoke(node, matched) + reportMatchStatus.invoke(node, expr, matched) } override fun reportPartialMatch(node: FlowValue, expr: Expression) { - reportPartialMatch.invoke(node) + reportPartialMatch.invoke(node, expr) } } diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 80190f007..759300d89 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -31,6 +31,7 @@ import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiModifierList import com.intellij.psi.SmartPointerManager @@ -62,7 +63,6 @@ import javax.swing.JLayeredPane import javax.swing.JPanel import javax.swing.JTextField import javax.swing.JToolBar -import javax.swing.SwingUtilities import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener import kotlinx.coroutines.Dispatchers @@ -166,7 +166,7 @@ private class FlowDiagramRef { private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: MethodNode): FlowDiagram { val diagramRef = FlowDiagramRef() - val graph = MxFlowGraph() + val graph = MxFlowGraph(flowGraph) setupStyles(graph) val groupedCells = addGraphContent(graph, flowGraph) val lineNumberNodes = sortedMapOf() @@ -343,10 +343,24 @@ private fun createSearchField(comp: mxGraphComponent, fixBounds: () -> Unit): JT return searchField } -private class MxFlowGraph : mxGraph() { - override fun getToolTipForCell(cell: Any?): String { +private class MxFlowGraph(private val flowGraph: FlowGraph) : mxGraph() { + override fun getToolTipForCell(cell: Any?): String? { val flow = (cell as? mxCell)?.value as? FlowNode ?: return super.getToolTipForCell(cell) - return flow.longText + if (!flowGraph.shouldShowTooltips()) { + return null + } + val lines = mutableListOf() + flow.currentMatchResult?.let { match -> + lines += match.toString() + } + lines += flow.longText + return lines.joinToString( + prefix = "", + separator = "

", + postfix = "" + ) { + StringUtil.escapeXmlEntities(it).replace("\n", "
") + } } override fun convertValueToString(cell: Any?): String { @@ -357,11 +371,11 @@ private class MxFlowGraph : mxGraph() { override fun getCellStyle(cell: Any?): MutableMap { val result = super.getCellStyle(cell).toMutableMap() val flow = (cell as? mxCell)?.value as? FlowNode ?: return result - when (flow.currentMatchStatus) { - MatchStatus.IGNORED -> result += DiagramStyles.IGNORED - MatchStatus.FAIL -> result += DiagramStyles.FAILED - MatchStatus.PARTIAL -> result += DiagramStyles.PARTIAL_MATCH - MatchStatus.SUCCESS -> result += DiagramStyles.SUCCESS + when (flow.currentMatchResult?.status) { + FlowMatchStatus.IGNORED -> result += DiagramStyles.IGNORED + FlowMatchStatus.FAIL -> result += DiagramStyles.FAILED + FlowMatchStatus.PARTIAL -> result += DiagramStyles.PARTIAL_MATCH + FlowMatchStatus.SUCCESS -> result += DiagramStyles.SUCCESS null -> {} } if (flow.searchHighlight) { diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt index 82c4c718e..4b5addfee 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt @@ -25,6 +25,8 @@ import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil import com.intellij.openapi.application.readAction import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression import com.llamalad7.mixinextras.expression.impl.flow.FlowValue import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander import java.util.SortedSet @@ -32,10 +34,24 @@ import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.LineNumberNode import org.objectweb.asm.tree.MethodNode -enum class MatchStatus { +enum class FlowMatchStatus { IGNORED, FAIL, PARTIAL, SUCCESS } +data class FlowMatchResult(val status: FlowMatchStatus, val attempted: String?) : Comparable { + override fun compareTo(other: FlowMatchResult) = status.compareTo(other.status) + + override fun toString(): String { + val attempted = '`' + StringUtil.escapeStringCharacters(attempted.toString()) + '`' + return when (status) { + FlowMatchStatus.IGNORED -> "Ignored" + FlowMatchStatus.FAIL -> "Failed to match $attempted" + FlowMatchStatus.PARTIAL -> "Partially matched $attempted" + FlowMatchStatus.SUCCESS -> "Successfully matched $attempted" + } + } +} + class FlowNode( val flow: FlowValue, project: Project, @@ -43,14 +59,15 @@ class FlowNode( method: MethodNode, map: MutableMap ) { - private val matches = mutableMapOf().withDefault { MatchStatus.IGNORED } - var currentMatchStatus: MatchStatus? = null + private val matches = + mutableMapOf().withDefault { FlowMatchResult(FlowMatchStatus.IGNORED, null) } + var currentMatchResult: FlowMatchResult? = null private set val inputs = (0..= MatchStatus.PARTIAL } + val matchScore get() = matches.values.count { it.status >= FlowMatchStatus.PARTIAL } init { map[flow] = this @@ -65,18 +82,24 @@ class FlowNode( } fun clearMatchHighlight() { - currentMatchStatus = null + currentMatchResult = null } - fun reportMatchStatus(childFlow: FlowValue, matched: Boolean) { - updateMatchStatus(childFlow, if (matched) MatchStatus.SUCCESS else MatchStatus.FAIL) + fun reportMatchStatus(childFlow: FlowValue, expr: Expression, matched: Boolean) { + updateMatchStatus( + childFlow, + FlowMatchResult( + if (matched) FlowMatchStatus.SUCCESS else FlowMatchStatus.FAIL, + expr.src.toString() + ) + ) } - fun reportPartialMatch(childFlow: FlowValue) { - updateMatchStatus(childFlow, MatchStatus.PARTIAL) + fun reportPartialMatch(childFlow: FlowValue, expr: Expression) { + updateMatchStatus(childFlow, FlowMatchResult(FlowMatchStatus.PARTIAL, expr.src.toString())) } - private fun updateMatchStatus(childFlow: FlowValue, status: MatchStatus) { + private fun updateMatchStatus(childFlow: FlowValue, status: FlowMatchResult) { matches.compute(childFlow) { _, oldStatus -> if (oldStatus == null) { status @@ -88,7 +111,7 @@ class FlowNode( fun highlightMatches(allNodes: Iterable) { for (node in allNodes) { - node.currentMatchStatus = matches.getValue(node.flow) + node.currentMatchResult = matches.getValue(node.flow) } } } @@ -170,6 +193,8 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN node.clearMatchHighlight() } } + + fun shouldShowTooltips() = !hasMatchData || hardHighlight } private val FlowValue.isRoot get() = next.isEmpty() From a2360e9e174218dfc690480218cb9d619db934bd Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 6 Jul 2025 00:29:04 +0100 Subject: [PATCH 06/13] Accessibility: Use different line thicknesses for match colours in flow diagrams. --- .../kotlin/platform/mixin/expression/gui/DiagramStyles.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt index 0b0f5721e..879d55e47 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt @@ -45,17 +45,17 @@ object DiagramStyles { val FAILED get() = mapOf( mxConstants.STYLE_STROKECOLOR to JBColor.red.hexString, - mxConstants.STYLE_STROKEWIDTH to "2", + mxConstants.STYLE_STROKEWIDTH to "3.5", ) val PARTIAL_MATCH get() = mapOf( mxConstants.STYLE_STROKECOLOR to JBColor.orange.hexString, - mxConstants.STYLE_STROKEWIDTH to "2", + mxConstants.STYLE_STROKEWIDTH to "2.5", ) val SUCCESS get() = mapOf( mxConstants.STYLE_STROKECOLOR to JBColor.green.hexString, - mxConstants.STYLE_STROKEWIDTH to "2", + mxConstants.STYLE_STROKEWIDTH to "1.5", ) } From b83ba83e9728bf62ef719afdb52f9bde657f04d7 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 6 Jul 2025 01:59:48 +0100 Subject: [PATCH 07/13] Improvement: Show expression text in flow graph UI and allow jumping back to its source. --- .../mixin/expression/gui/FlowDiagram.kt | 162 +++++++++++------- 1 file changed, 98 insertions(+), 64 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 759300d89..df0cf6a71 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -28,15 +28,15 @@ import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil +import com.intellij.pom.Navigatable import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiModifierList import com.intellij.psi.SmartPointerManager -import com.intellij.ui.components.JBLayeredPane -import com.intellij.util.ui.JBUI import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import com.mxgraph.layout.hierarchical.mxHierarchicalLayout import com.mxgraph.model.mxCell @@ -45,21 +45,18 @@ import com.mxgraph.util.mxEvent import com.mxgraph.util.mxRectangle import com.mxgraph.view.mxGraph import java.awt.BorderLayout -import java.awt.Component import java.awt.Dimension import java.awt.FlowLayout import java.awt.Rectangle -import java.awt.event.ComponentAdapter -import java.awt.event.ComponentEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.util.SortedMap import java.util.concurrent.Callable +import javax.swing.BorderFactory +import javax.swing.BoxLayout import javax.swing.Icon import javax.swing.JButton -import javax.swing.JComponent import javax.swing.JLabel -import javax.swing.JLayeredPane import javax.swing.JPanel import javax.swing.JTextField import javax.swing.JToolBar @@ -92,6 +89,8 @@ class FlowDiagram( var matchExpression: ((jump: Boolean) -> Unit) = {} private set + var jumpToExpression: () -> Unit = {} + private set fun populateMatchStatuses( module: Module, @@ -103,13 +102,13 @@ class FlowDiagram( SmartPointerManager.getInstance(module.project).createSmartPsiElementPointer(currentModifierList) this.matchExpression = { jump -> val oldHighlightRoot = flowGraph.highlightRoot - callbacks.setButtonsVisible(false) + callbacks.setMatchToolbarVisible(false) flowGraph.resetMatches() - ReadAction.nonBlocking(Callable run@{ - val stringLit = stringRef.element ?: return@run - val modifierList = modifierListRef.element ?: return@run + ReadAction.nonBlocking(Callable run@{ + val stringLit = stringRef.element ?: return@run null + val modifierList = modifierListRef.element ?: return@run null val expression = stringLit.constantStringValue?.let(MEExpressionMatchUtil::createExpression) - ?: return@run + ?: return@run null val pool = MEExpressionMatchUtil.createIdentifierPoolFactory(module, clazz, modifierList)(method) for ((virtualInsn, root) in flowGraph.flowMap) { val node = flowGraph.allNodes.getValue(root) @@ -123,16 +122,27 @@ class FlowDiagram( } flowGraph.markHasMatchData() flowGraph.highlightMatches(oldHighlightRoot, false) + StringUtil.escapeStringCharacters(expression.src.toString()) }) - .finishOnUiThread(ModalityState.nonModal()) { + .finishOnUiThread(ModalityState.nonModal()) { exprText -> + exprText ?: return@finishOnUiThread if (jump) { showBestNode() } comp.refresh() - callbacks.setButtonsVisible(true) + callbacks.setExprText(exprText) + callbacks.setMatchToolbarVisible(true) } .submit(ApplicationManager.getApplication()::executeOnPooledThread) } + this.jumpToExpression = { + ReadAction.run { + val target = stringRef.element + if (target is Navigatable && target.isValid && target.canNavigate()) { + target.navigate(true) + } + } + } matchExpression(true) } @@ -147,13 +157,19 @@ class FlowDiagram( } fun clearExpression() { - callbacks.setButtonsVisible(false) + callbacks.setMatchToolbarVisible(false) flowGraph.resetMatches() comp.refresh() + matchExpression = {} + jumpToExpression = {} } } -class FlowDiagramCallbacks(val scrollToLine: (Int) -> Unit, val setButtonsVisible: (Boolean) -> Unit) +class FlowDiagramCallbacks( + val scrollToLine: (Int) -> Unit, + val setMatchToolbarVisible: (Boolean) -> Unit, + val setExprText: (String) -> Unit +) private class FlowDiagramRef { lateinit var diagram: FlowDiagram @@ -199,15 +215,8 @@ private fun displayGraphComponent( fixBounds() configureGraphComponent(comp, flowGraph) - val toolbar = createToolbar(comp, ::fixBounds) - panel.add(toolbar, BorderLayout.NORTH) - - val container = JBLayeredPane().apply { - add(comp, JLayeredPane.DEFAULT_LAYER as Any) - } - - val buttonWrapper = setupFloatingButtons(diagramRef, comp, container) - panel.add(container, BorderLayout.CENTER) + val (matchToolbar, setExprText) = createToolbars(diagramRef, comp, panel, ::fixBounds) + panel.add(comp, BorderLayout.CENTER) return comp to FlowDiagramCallbacks( scrollToLine = { lineNumber -> @@ -215,48 +224,13 @@ private fun displayGraphComponent( scrollCellToVisible(comp, node) } }, - setButtonsVisible = { visible -> - buttonWrapper.isVisible = visible + setMatchToolbarVisible = { visible -> + matchToolbar.isVisible = visible }, + setExprText = setExprText, ) } -private fun setupFloatingButtons(diagramRef: FlowDiagramRef, comp: mxGraphComponent, container: JBLayeredPane): JComponent { - val refreshButton = makeButton(AllIcons.Actions.Refresh, "Re-match Expression") { - diagramRef.diagram.matchExpression(false) - } - - val closeButton = makeButton(AllIcons.Actions.CloseDarkGrey, "Clear Match Data") { - diagramRef.diagram.clearExpression() - } - - val buttonWrapper = JPanel().apply { - isVisible = false - layout = FlowLayout(FlowLayout.RIGHT, 3, 5) - alignmentX = Component.RIGHT_ALIGNMENT - alignmentY = Component.TOP_ALIGNMENT - border = JBUI.Borders.empty(4) - add(refreshButton) - add(closeButton) - } - - container.add(buttonWrapper, JLayeredPane.PALETTE_LAYER as Any) - - container.addComponentListener(object : ComponentAdapter() { - override fun componentResized(e: ComponentEvent) { - comp.setBounds(0, 0, container.width, container.height) - - val parentWidth = container.width - val childWidth = buttonWrapper.preferredSize.width - val childHeight = buttonWrapper.preferredSize.height - val margin = 20 - - buttonWrapper.setBounds(parentWidth - childWidth - margin, margin, childWidth, childHeight) - } - }) - return buttonWrapper -} - private fun makeButton(icon: Icon, tooltip: String, action: () -> Unit): JButton = JButton(icon).apply { toolTipText = tooltip @@ -279,7 +253,24 @@ private fun scrollCellToVisible(comp: mxGraphComponent, node: mxCell) { comp.graphControl.scrollRectToVisible(targetRect) } -private fun createToolbar(comp: mxGraphComponent, fixBounds: () -> Unit): JToolBar { +private fun createToolbars( + diagramRef: FlowDiagramRef, + comp: mxGraphComponent, + panel: JPanel, + fixBounds: () -> Unit +): Pair Unit> { + val container = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + } + container.add(createViewToolbar(comp, fixBounds)) + val (matchToolbar, setExprText) = createMatchToolbar(diagramRef) + container.add(matchToolbar) + + panel.add(container, BorderLayout.NORTH) + return matchToolbar to setExprText +} + +private fun createViewToolbar(comp: mxGraphComponent, fixBounds: () -> Unit): JToolBar { val toolbar = JToolBar() toolbar.isFloatable = false val zoomInButton = JButton("+") @@ -300,6 +291,49 @@ private fun createToolbar(comp: mxGraphComponent, fixBounds: () -> Unit): JToolB return toolbar } +private fun createMatchToolbar(diagramRef: FlowDiagramRef): Pair Unit> { + val helpLabel = JLabel("Showing matches for:").apply { + border = BorderFactory.createEmptyBorder(0, 6, 0, 0) + } + + val exprText = JLabel(" ").apply { + font = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) + border = BorderFactory.createEmptyBorder(0, 15, 0, 5) + this.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (e.clickCount == 2) { + diagramRef.diagram.jumpToExpression() + } + } + }) + } + + val refreshButton = makeButton(AllIcons.Actions.Refresh, "Re-match Expression") { + diagramRef.diagram.matchExpression(false) + } + val closeButton = makeButton(AllIcons.Actions.CloseDarkGrey, "Clear Match Data") { + diagramRef.diagram.clearExpression() + } + val buttonPanel = JPanel().apply { + layout = FlowLayout(FlowLayout.RIGHT, 3, 3) + isOpaque = false + add(refreshButton) + add(closeButton) + } + + return JToolBar().apply { + isVisible = false + isFloatable = false + layout = BorderLayout() + add(helpLabel, BorderLayout.WEST) + add(exprText, BorderLayout.CENTER) + add(buttonPanel, BorderLayout.EAST) + } to { + exprText.text = it + exprText.toolTipText = it + } +} + private fun createSearchField(comp: mxGraphComponent, fixBounds: () -> Unit): JTextField { val graph = comp.graph val searchField = JTextField() From 2b9f231682f19aa3263a5198db157b2cc933f7d6 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 6 Jul 2025 02:17:16 +0100 Subject: [PATCH 08/13] Improvement: Display expression source in tooltips using editor font. --- .../mixin/expression/gui/DiagramStyles.kt | 4 +++- .../platform/mixin/expression/gui/FlowDiagram.kt | 15 ++++++++------- .../platform/mixin/expression/gui/FlowGraph.kt | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt index 879d55e47..be1faa85f 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt @@ -11,7 +11,7 @@ import java.awt.Color object DiagramStyles { val DEFAULT_NODE get() = mapOf( - mxConstants.STYLE_FONTFAMILY to EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN).family, + mxConstants.STYLE_FONTFAMILY to CURRENT_EDITOR_FONT.family, mxConstants.STYLE_ROUNDED to true, mxConstants.STYLE_FILLCOLOR to JBUI.CurrentTheme.Button.buttonColorStart().hexString, mxConstants.STYLE_FONTCOLOR to UIUtil.getLabelForeground().hexString, @@ -57,6 +57,8 @@ object DiagramStyles { mxConstants.STYLE_STROKECOLOR to JBColor.green.hexString, mxConstants.STYLE_STROKEWIDTH to "1.5", ) + val CURRENT_EDITOR_FONT + get() = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) } private val Color.hexString get() = "#%06X".format(rgb) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index df0cf6a71..db74e2876 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -28,7 +28,6 @@ import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project @@ -297,7 +296,7 @@ private fun createMatchToolbar(diagramRef: FlowDiagramRef): Pair() flow.currentMatchResult?.let { match -> - lines += match.toString() + lines += match.toString( + prefix = "`", + suffix = "`", + transform = StringUtil::escapeXmlEntities + ) } - lines += flow.longText + lines += StringUtil.escapeXmlEntities(flow.longText).replace("\n", "
") return lines.joinToString( prefix = "", separator = "

", postfix = "" - ) { - StringUtil.escapeXmlEntities(it).replace("\n", "
") - } + ) } override fun convertValueToString(cell: Any?): String { diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt index 4b5addfee..ada6b889d 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt @@ -41,8 +41,8 @@ enum class FlowMatchStatus { data class FlowMatchResult(val status: FlowMatchStatus, val attempted: String?) : Comparable { override fun compareTo(other: FlowMatchResult) = status.compareTo(other.status) - override fun toString(): String { - val attempted = '`' + StringUtil.escapeStringCharacters(attempted.toString()) + '`' + fun toString(prefix: String, suffix: String, transform: (String) -> String): String { + val attempted = prefix + transform(StringUtil.escapeStringCharacters(attempted.toString())) + suffix return when (status) { FlowMatchStatus.IGNORED -> "Ignored" FlowMatchStatus.FAIL -> "Failed to match $attempted" From 98a110301f463b76334e9dfdef2f628958afb851 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 6 Jul 2025 14:21:36 +0100 Subject: [PATCH 09/13] Fix: Fix NPE when searching flow graph without a selection. --- .../mixin/expression/gui/FlowDiagram.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index db74e2876..5b6527ba7 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -352,8 +352,8 @@ private fun createSearchField(comp: mxGraphComponent, fixBounds: () -> Unit): JT for (cell in vertices) { val flow = (cell as mxCell).value as? FlowNode ?: continue val texts = listOf( - graph.convertValueToString(cell), - graph.getToolTipForCell(cell), + flow.shortText, + flow.longText, ) if (searchText.isNotEmpty() && texts.any { searchText in it.lowercase() }) { @@ -379,16 +379,15 @@ private fun createSearchField(comp: mxGraphComponent, fixBounds: () -> Unit): JT private class MxFlowGraph(private val flowGraph: FlowGraph) : mxGraph() { override fun getToolTipForCell(cell: Any?): String? { val flow = (cell as? mxCell)?.value as? FlowNode ?: return super.getToolTipForCell(cell) - if (!flowGraph.shouldShowTooltips()) { - return null - } val lines = mutableListOf() - flow.currentMatchResult?.let { match -> - lines += match.toString( - prefix = "`", - suffix = "`", - transform = StringUtil::escapeXmlEntities - ) + if (flowGraph.shouldShowTooltips()) { + flow.currentMatchResult?.let { match -> + lines += match.toString( + prefix = "`", + suffix = "`", + transform = StringUtil::escapeXmlEntities + ) + } } lines += StringUtil.escapeXmlEntities(flow.longText).replace("\n", "
") return lines.joinToString( From f06e59a7e2667ee8fd13b91a816aba8cb157a347 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 18 Jul 2025 16:09:20 +0100 Subject: [PATCH 10/13] Improvement: Add popup when there is a choice between multiple target methods. --- .../mixin/expression/gui/FlowStrings.kt | 12 +- .../expression/gui/MEFlowWindowService.kt | 1 + .../mixin/expression/gui/MEShowFlowAction.kt | 124 +++++++++++------- .../reference/AbstractMethodReference.kt | 2 +- .../kotlin/platform/mixin/util/AsmUtil.kt | 6 + 5 files changed, 90 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt index 21a564496..2dec83ba6 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt @@ -21,6 +21,8 @@ package com.demonwav.mcdev.platform.mixin.expression.gui import com.demonwav.mcdev.platform.mixin.util.LocalVariables +import com.demonwav.mcdev.platform.mixin.util.shortDescString +import com.demonwav.mcdev.platform.mixin.util.shortName import com.demonwav.mcdev.platform.mixin.util.textify import com.intellij.openapi.application.ReadAction import com.intellij.openapi.project.Project @@ -81,14 +83,8 @@ fun FlowValue.longString(): String = insn.textify() } -val Type.shortName get() = className.substringAfterLast('.').replace('$', '.') - private fun shortOwner(owner: String) = Type.getObjectType(owner).shortName -private fun shortParams(desc: String) = Type.getArgumentTypes(desc).joinToString(prefix = "(", postfix = ")") { - it.shortName -} - private fun constantString(cst: Any): String { if (cst is Int) { return when (cst) { @@ -115,7 +111,7 @@ private fun instantiationString(info: InstantiationInfo): String { return start } val call = insn.insn as? MethodInsnNode ?: return start - return start + shortParams(call.desc) + return start + shortDescString(call.desc) } private fun lmfString(info: LMFInfo): String { @@ -154,7 +150,7 @@ private fun methodString(insn: MethodInsnNode, methodCallType: MethodCallType): MethodCallType.SUPER -> "super" MethodCallType.STATIC -> shortOwner(insn.owner) } - return "$owner.${insn.name}${shortParams(insn.desc)}" + return "$owner.${insn.name}${shortDescString(insn.desc)}" } private fun castString(type: Type): String = "(${type.shortName})" diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt index c795b4a84..fdd9492f9 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.expression.gui +import com.demonwav.mcdev.platform.mixin.util.shortName import com.intellij.openapi.application.EDT import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt index 0f82c4e4f..5a3445226 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt @@ -20,16 +20,18 @@ package com.demonwav.mcdev.platform.mixin.expression.gui -import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.reference.MethodReference import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember import com.demonwav.mcdev.platform.mixin.util.findClassNodeByPsiClass +import com.demonwav.mcdev.platform.mixin.util.isMixin import com.demonwav.mcdev.platform.mixin.util.mixinTargets -import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.platform.mixin.util.shortDescString +import com.demonwav.mcdev.platform.mixin.util.shortName import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.ifEmpty import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.lang.java.JavaLanguage import com.intellij.openapi.actionSystem.ActionUpdateThread @@ -38,6 +40,9 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.components.service +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.PopupStep +import com.intellij.openapi.ui.popup.util.BaseListPopupStep import com.intellij.psi.PsiClass import com.intellij.psi.PsiIdentifier import com.intellij.psi.PsiLiteralExpression @@ -52,81 +57,108 @@ class MEShowFlowAction : AnAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT override fun update(e: AnActionEvent) { - e.presentation.isEnabledAndVisible = resolve(e) != null + e.presentation.isEnabledAndVisible = resolve(e).isNotEmpty() } override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return - val (clazz, method, action) = resolve(e) ?: return - project.service().showDiagram(clazz, method, action) + val results = resolve(e).ifEmpty { return } + + fun navigate(resolved: Resolved) { + project.service().showDiagram(resolved.clazz, resolved.method, resolved.action) + } + + results.singleOrNull()?.let { return navigate(it) } + + val step = object : BaseListPopupStep("Choose Target Method", results) { + override fun onChosen(selectedValue: Resolved, finalChoice: Boolean): PopupStep<*>? { + return doFinalStep { + navigate(selectedValue) + } + } + } + + JBPopupFactory.getInstance().createListPopup(step).showInBestPositionFor(e.dataContext) } - private fun resolve(e: AnActionEvent): Resolved? { - val project = e.project ?: return null - val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null + private fun resolve(e: AnActionEvent): List { + val project = e.project ?: return emptyList() + val file = e.getData(CommonDataKeys.PSI_FILE) ?: return emptyList() if (file.language != JavaLanguage.INSTANCE) { - return null + return emptyList() } - val caret = e.getData(CommonDataKeys.CARET) ?: return null - val element = file.findElementAt(caret.offset) ?: return null - val psiClass = element.parentOfType() ?: return null + val caret = e.getData(CommonDataKeys.CARET) ?: return emptyList() + val element = file.findElementAt(caret.offset) ?: return emptyList() + val psiClass = element.parentOfType() ?: return emptyList() - fun resolveMixinMethodString(): Resolved? { - val string = element.parentOfType() ?: return null - return MethodReference.resolveIfUnique(string)?.let { (clazz, method) -> + fun resolveMixinMethodString(): Sequence { + val string = element.parentOfType() ?: return emptySequence() + return MethodReference.resolve(string)?.map { (clazz, method) -> Resolved(clazz, method) - } + }.orEmpty() } - fun resolvePsiMethod(): Resolved? { - val identifier = element as? PsiIdentifier ?: return null - val psiMethod = identifier.parent as? PsiMethod ?: return null - val clazz = findClassNodeByPsiClass(psiClass) ?: return null - val desc = psiMethod.descriptor ?: return null - val methodNode = clazz.methods.find { it.name == psiMethod.name && it.desc == desc } ?: return null - return Resolved(clazz, methodNode) + fun resolvePsiMethod(): Sequence { + val identifier = element as? PsiIdentifier ?: return emptySequence() + val psiMethod = identifier.parent as? PsiMethod ?: return emptySequence() + val clazz = findClassNodeByPsiClass(psiClass) ?: return emptySequence() + val desc = psiMethod.descriptor ?: return emptySequence() + return clazz.methods.asSequence() + .filter { it.name == psiMethod.name && it.desc == desc } + .map { Resolved(clazz, it) } } - fun resolveMethodByLine(): Resolved? { - val clazz = findClassNodeByPsiClass(psiClass) ?: return null + fun resolveMethodByLine(): Sequence { + val clazz = findClassNodeByPsiClass(psiClass) ?: return emptySequence() val lineNumber = caret.logicalPosition.line + 1 - val method = clazz.methods.lastOrNull { method -> + val methods = clazz.methods.asSequence().filter { method -> method.instructions.asSequence() .filterIsInstance() .any { it.line == lineNumber } - } ?: return null - return Resolved(clazz, method) { - it.callbacks.scrollToLine(lineNumber) + } + return methods.map { method -> + Resolved(clazz, method) { + it.callbacks.scrollToLine(lineNumber) + } } } - fun resolveExpressionTarget(): Resolved? { - val module = e.getData(LangDataKeys.MODULE) ?: return null - val string = element.parentOfType() ?: return null - val modifierList = string.parentOfType()?.modifierList ?: return null + fun resolveExpressionTarget(): Sequence { + val module = e.getData(LangDataKeys.MODULE) ?: return emptySequence() + val string = element.parentOfType() ?: return emptySequence() + val modifierList = string.parentOfType()?.modifierList ?: return emptySequence() if (InjectedLanguageManager.getInstance(project).getInjectedPsiFiles(string).orEmpty() .none { it.first is MEExpressionFile } ) { - return null + return emptySequence() } val (injectorAnnotation, injector) = modifierList.annotations.firstNotNullOfOrNull { ann -> (MixinAnnotationHandler.forMixinAnnotation(ann, project) as? InjectorAnnotationHandler) ?.let { ann to it } - } ?: return null - val targetClass = psiClass.mixinTargets.singleOrNull() ?: return null - val target = injector.resolveTarget(injectorAnnotation, targetClass).singleOrNull() - as? MethodTargetMember ?: return null - return Resolved(target.classAndMethod.clazz, target.classAndMethod.method) { - it.populateMatchStatuses(module, string, modifierList) - } + } ?: return emptySequence() + return psiClass.mixinTargets.asSequence() + .flatMap { injector.resolveTarget(injectorAnnotation, it) } + .filterIsInstance() + .map { target -> + Resolved(target.classAndMethod.clazz, target.classAndMethod.method) { + it.populateMatchStatuses(module, string, modifierList) + } + } } - return resolveExpressionTarget() - ?: resolveMixinMethodString() - ?: resolvePsiMethod() - ?: resolveMethodByLine() + return buildList { + if (psiClass.isMixin) { + addAll(resolveExpressionTarget()) + addAll(resolveMixinMethodString()) + } else { + addAll(resolvePsiMethod()) + addAll(resolveMethodByLine()) + } + } } - private data class Resolved(val clazz: ClassNode, val method: MethodNode, val action: (FlowDiagram) -> Unit = {}) + private data class Resolved(val clazz: ClassNode, val method: MethodNode, val action: (FlowDiagram) -> Unit = {}) { + override fun toString() = "${clazz.shortName}::${method.name}${shortDescString(method.desc)}" + } } diff --git a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt index 029deadb6..b253c825b 100644 --- a/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt +++ b/src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt @@ -105,7 +105,7 @@ abstract class AbstractMethodReference : PolyReferenceResolver(), MixinReference return targets.any { it.findMethods(MemberReference(targetReference.name)).count() > 1 } } - private fun resolve(context: PsiElement): Sequence? { + fun resolve(context: PsiElement): Sequence? { val targets = getTargets(context) ?: return null val targetedMethods = when (context) { is PsiArrayInitializerMemberValue -> context.initializers.mapNotNull { it.constantStringValue } diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt index 6656f60cf..b2f6c7d05 100644 --- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt @@ -183,6 +183,12 @@ fun internalNameToShortName(internalName: String) = internalName.substringAfterL val ClassNode.shortName get() = internalNameToShortName(name) +val Type.shortName get() = className.substringAfterLast('.').replace('$', '.') + +fun shortDescString(desc: String) = Type.getArgumentTypes(desc).joinToString(prefix = "(", postfix = ")") { + it.shortName +} + private val LOAD_CLASS_FILE_BYTES: Method? = runCatching { com.intellij.byteCodeViewer.ByteCodeViewerManager::class.java .getDeclaredMethod("loadClassFileBytes", PsiClass::class.java) From ebf23d9a15395d6a976cc29124150ec6fccebe51 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 18 Jul 2025 17:06:19 +0100 Subject: [PATCH 11/13] Cleanup: Use MVC for FlowDiagram. --- .../mixin/expression/gui/DiagramStyles.kt | 20 ++ .../mixin/expression/gui/FlowDiagram.kt | 318 ++---------------- .../mixin/expression/gui/FlowDiagramUi.kt | 279 +++++++++++++++ .../mixin/expression/gui/GraphUtils.kt | 32 ++ .../expression/gui/MEFlowWindowService.kt | 2 +- .../mixin/expression/gui/MEShowFlowAction.kt | 2 +- 6 files changed, 367 insertions(+), 286 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt create mode 100644 src/main/kotlin/platform/mixin/expression/gui/GraphUtils.kt diff --git a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt index be1faa85f..657198b36 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt @@ -1,3 +1,23 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + package com.demonwav.mcdev.platform.mixin.expression.gui import com.intellij.openapi.editor.colors.EditorColorsManager diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 5b6527ba7..575999950 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -22,12 +22,10 @@ package com.demonwav.mcdev.platform.mixin.expression.gui import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil import com.demonwav.mcdev.util.constantStringValue -import com.intellij.icons.AllIcons import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ReadAction -import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project @@ -39,28 +37,11 @@ import com.intellij.psi.SmartPointerManager import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import com.mxgraph.layout.hierarchical.mxHierarchicalLayout import com.mxgraph.model.mxCell -import com.mxgraph.swing.mxGraphComponent -import com.mxgraph.util.mxEvent import com.mxgraph.util.mxRectangle import com.mxgraph.view.mxGraph -import java.awt.BorderLayout import java.awt.Dimension -import java.awt.FlowLayout -import java.awt.Rectangle -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent import java.util.SortedMap import java.util.concurrent.Callable -import javax.swing.BorderFactory -import javax.swing.BoxLayout -import javax.swing.Icon -import javax.swing.JButton -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextField -import javax.swing.JToolBar -import javax.swing.event.DocumentEvent -import javax.swing.event.DocumentListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.objectweb.asm.tree.ClassNode @@ -72,12 +53,10 @@ private const val INTRA_GROUP_SPACING = 75 private const val LINE_NUMBER_STYLE = "LINE_NUMBER" class FlowDiagram( - private val comp: mxGraphComponent, + val ui: FlowDiagramUi, private val flowGraph: FlowGraph, private val clazz: ClassNode, val method: MethodNode, - val panel: JPanel, - val callbacks: FlowDiagramCallbacks, ) { companion object { suspend fun create(project: Project, clazz: ClassNode, method: MethodNode): FlowDiagram? { @@ -91,6 +70,29 @@ class FlowDiagram( var jumpToExpression: () -> Unit = {} private set + init { + ui.viewToolbar.onSearchFieldChanged { + ui.highlightCells(it) + } + + ui.matchToolbar.onTextClicked { + jumpToExpression() + } + + ui.matchToolbar.onRefresh { + matchExpression(false) + } + + ui.matchToolbar.onClear { + clearExpression() + } + + ui.onNodeSelected { node, soft -> + flowGraph.highlightMatches(node, soft) + ui.refresh() + } + } + fun populateMatchStatuses( module: Module, currentStringLit: PsiLiteralExpression, @@ -101,7 +103,7 @@ class FlowDiagram( SmartPointerManager.getInstance(module.project).createSmartPsiElementPointer(currentModifierList) this.matchExpression = { jump -> val oldHighlightRoot = flowGraph.highlightRoot - callbacks.setMatchToolbarVisible(false) + ui.setMatchToolbarVisible(false) flowGraph.resetMatches() ReadAction.nonBlocking(Callable run@{ val stringLit = stringRef.element ?: return@run null @@ -128,9 +130,8 @@ class FlowDiagram( if (jump) { showBestNode() } - comp.refresh() - callbacks.setExprText(exprText) - callbacks.setMatchToolbarVisible(true) + ui.refresh() + ui.setExprText(exprText) } .submit(ApplicationManager.getApplication()::executeOnPooledThread) } @@ -147,233 +148,30 @@ class FlowDiagram( private fun showBestNode() { val bestNode = flowGraph.allNodes.values.maxBy { it.matchScore } - val bestCell = comp.graph.getChildVertices(comp.graph.defaultParent).asSequence() - .map { it as mxCell } - .find { it.value === bestNode } - ?: return flowGraph.highlightMatches(bestNode, false) - comp.scrollCellToVisible(bestCell, true) + ui.scrollToNode(bestNode) } - fun clearExpression() { - callbacks.setMatchToolbarVisible(false) + private fun clearExpression() { + ui.setMatchToolbarVisible(false) flowGraph.resetMatches() - comp.refresh() + ui.refresh() matchExpression = {} jumpToExpression = {} } } -class FlowDiagramCallbacks( - val scrollToLine: (Int) -> Unit, - val setMatchToolbarVisible: (Boolean) -> Unit, - val setExprText: (String) -> Unit -) - -private class FlowDiagramRef { - lateinit var diagram: FlowDiagram - private set - - fun bind(newDiagram: FlowDiagram) { - diagram = newDiagram - } -} - private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: MethodNode): FlowDiagram { - val diagramRef = FlowDiagramRef() val graph = MxFlowGraph(flowGraph) setupStyles(graph) val groupedCells = addGraphContent(graph, flowGraph) val lineNumberNodes = sortedMapOf() val calculateBounds = layOutGraph(graph, groupedCells, lineNumberNodes) - val panel: JPanel - val (comp, callbacks) = withContext(Dispatchers.EDT) { - panel = JPanel(BorderLayout()) - displayGraphComponent(diagramRef, flowGraph, graph, panel, calculateBounds, lineNumberNodes) + val ui = withContext(Dispatchers.EDT) { + FlowDiagramUi(graph, calculateBounds, lineNumberNodes) } - return FlowDiagram(comp, flowGraph, clazz, method, panel, callbacks).also(diagramRef::bind) -} - -private fun displayGraphComponent( - diagramRef: FlowDiagramRef, - flowGraph: FlowGraph, - graph: mxGraph, - panel: JPanel, - calculateBounds: () -> Dimension, - lineNumberNodes: SortedMap -): Pair { - val comp = mxGraphComponent(graph) - fun fixBounds() { - comp.graphControl.preferredSize = calculateBounds() - } - - graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE) { _, _ -> - fixBounds() - } - fixBounds() - configureGraphComponent(comp, flowGraph) - - val (matchToolbar, setExprText) = createToolbars(diagramRef, comp, panel, ::fixBounds) - panel.add(comp, BorderLayout.CENTER) - - return comp to FlowDiagramCallbacks( - scrollToLine = { lineNumber -> - lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) -> - scrollCellToVisible(comp, node) - } - }, - setMatchToolbarVisible = { visible -> - matchToolbar.isVisible = visible - }, - setExprText = setExprText, - ) -} - -private fun makeButton(icon: Icon, tooltip: String, action: () -> Unit): JButton = - JButton(icon).apply { - toolTipText = tooltip - preferredSize = Dimension(32, 32) - addActionListener { - action() - } - } - -private fun scrollCellToVisible(comp: mxGraphComponent, node: mxCell) { - // Scrolls the cell to the top of the screen if possible - val graph = comp.graph - val state = graph.view.getState(node) ?: return - val cellBounds = state.rectangle - val viewRect = comp.viewport.viewRect - val targetRect = Rectangle( - cellBounds.x, cellBounds.y, - 1, viewRect.height - ) - comp.graphControl.scrollRectToVisible(targetRect) -} - -private fun createToolbars( - diagramRef: FlowDiagramRef, - comp: mxGraphComponent, - panel: JPanel, - fixBounds: () -> Unit -): Pair Unit> { - val container = JPanel().apply { - layout = BoxLayout(this, BoxLayout.Y_AXIS) - } - container.add(createViewToolbar(comp, fixBounds)) - val (matchToolbar, setExprText) = createMatchToolbar(diagramRef) - container.add(matchToolbar) - - panel.add(container, BorderLayout.NORTH) - return matchToolbar to setExprText -} - -private fun createViewToolbar(comp: mxGraphComponent, fixBounds: () -> Unit): JToolBar { - val toolbar = JToolBar() - toolbar.isFloatable = false - val zoomInButton = JButton("+") - zoomInButton.toolTipText = "Zoom In" - zoomInButton.addActionListener { - comp.zoomIn() - } - val zoomOutButton = JButton("−") - zoomOutButton.toolTipText = "Zoom Out" - zoomOutButton.addActionListener { - comp.zoomOut() - } - toolbar.add(zoomInButton) - toolbar.add(zoomOutButton) - toolbar.addSeparator(Dimension(20, 0)) - toolbar.add(JLabel("Search: ")) - toolbar.add(createSearchField(comp, fixBounds)) - return toolbar -} - -private fun createMatchToolbar(diagramRef: FlowDiagramRef): Pair Unit> { - val helpLabel = JLabel("Showing matches for:").apply { - border = BorderFactory.createEmptyBorder(0, 6, 0, 0) - } - - val exprText = JLabel(" ").apply { - font = DiagramStyles.CURRENT_EDITOR_FONT - border = BorderFactory.createEmptyBorder(0, 15, 0, 5) - this.addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - if (e.clickCount == 2) { - diagramRef.diagram.jumpToExpression() - } - } - }) - } - - val refreshButton = makeButton(AllIcons.Actions.Refresh, "Re-match Expression") { - diagramRef.diagram.matchExpression(false) - } - val closeButton = makeButton(AllIcons.Actions.CloseDarkGrey, "Clear Match Data") { - diagramRef.diagram.clearExpression() - } - val buttonPanel = JPanel().apply { - layout = FlowLayout(FlowLayout.RIGHT, 3, 3) - isOpaque = false - add(refreshButton) - add(closeButton) - } - - return JToolBar().apply { - isVisible = false - isFloatable = false - layout = BorderLayout() - add(helpLabel, BorderLayout.WEST) - add(exprText, BorderLayout.CENTER) - add(buttonPanel, BorderLayout.EAST) - } to { - exprText.text = it - exprText.toolTipText = it - } -} - -private fun createSearchField(comp: mxGraphComponent, fixBounds: () -> Unit): JTextField { - val graph = comp.graph - val searchField = JTextField() - searchField.document.addDocumentListener(object : DocumentListener { - override fun insertUpdate(e: DocumentEvent) = updateHighlight() - - override fun removeUpdate(e: DocumentEvent) = updateHighlight() - - override fun changedUpdate(e: DocumentEvent) = updateHighlight() - - private fun updateHighlight() { - val searchText = searchField.text.lowercase() - graph.update { - val vertices = graph.getChildVertices(graph.defaultParent) - var scrolled = false - - for (cell in vertices) { - val flow = (cell as mxCell).value as? FlowNode ?: continue - val texts = listOf( - flow.shortText, - flow.longText, - ) - - if (searchText.isNotEmpty() && texts.any { searchText in it.lowercase() }) { - flow.searchHighlight = true - if (!scrolled) { - comp.scrollCellToVisible(cell, true) - comp.zoomTo(1.2, true) - graph.selectionCell = cell - scrolled = true - } - } else { - flow.searchHighlight = false - } - } - } - comp.refresh() - fixBounds() - } - }) - return searchField + return FlowDiagram(ui, flowGraph, clazz, method) } private class MxFlowGraph(private val flowGraph: FlowGraph) : mxGraph() { @@ -502,51 +300,3 @@ private fun setupStyles(graph: mxGraph) { stylesheet.defaultEdgeStyle.putAll(DiagramStyles.DEFAULT_EDGE) stylesheet.putCellStyle(LINE_NUMBER_STYLE, DiagramStyles.LINE_NUMBER) } - -private fun configureGraphComponent(comp: mxGraphComponent, flowGraph: FlowGraph) { - val graph = comp.graph - graph.isCellsSelectable = false - graph.isCellsEditable = false - comp.isConnectable = false - comp.isPanning = true - comp.setToolTips(true) - comp.viewport.setOpaque(true) - comp.viewport.setBackground(EditorColorsManager.getInstance().globalScheme.defaultBackground) - - comp.zoomAndCenter() - comp.graphControl.isDoubleBuffered = false - comp.graphControl.setOpaque(false) - comp.verticalScrollBar.setUnitIncrement(16) - comp.horizontalScrollBar.setUnitIncrement(16) - - configureMouseListeners(comp, flowGraph) -} - -private fun configureMouseListeners(comp: mxGraphComponent, flowGraph: FlowGraph) { - fun highlight(e: MouseEvent, soft: Boolean) { - val node = (comp.getCellAt(e.x, e.y) as mxCell?)?.value as? FlowNode - flowGraph.highlightMatches(node, soft) - comp.refresh() - e.consume() - } - - comp.graphControl.addMouseListener(object : MouseAdapter() { - override fun mousePressed(e: MouseEvent) { - highlight(e, false) - } - }) - comp.graphControl.addMouseMotionListener(object : MouseAdapter() { - override fun mouseMoved(e: MouseEvent) { - highlight(e, true) - } - }) -} - -private inline fun mxGraph.update(routine: () -> T): T { - model.beginUpdate() - try { - return routine() - } finally { - model.endUpdate() - } -} diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt new file mode 100644 index 000000000..1f305a2c7 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt @@ -0,0 +1,279 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.gui + +import com.intellij.icons.AllIcons +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.ui.DocumentAdapter +import com.mxgraph.model.mxCell +import com.mxgraph.swing.mxGraphComponent +import com.mxgraph.util.mxEvent +import com.mxgraph.view.mxGraph +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.FlowLayout +import java.awt.Rectangle +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.util.SortedMap +import javax.swing.BorderFactory +import javax.swing.BoxLayout +import javax.swing.Icon +import javax.swing.JButton +import javax.swing.JLabel +import javax.swing.JPanel +import javax.swing.JTextField +import javax.swing.JToolBar +import javax.swing.event.DocumentEvent + +class FlowDiagramUi( + private val graph: mxGraph, + private val calculateBounds: () -> Dimension, + private val lineNumberNodes: SortedMap, +) : JPanel(BorderLayout()) { + private val comp = mxGraphComponent(graph) + val viewToolbar = ViewToolbar() + val matchToolbar = MatchToolbar() + + private val toolbars = JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(viewToolbar) + add(matchToolbar) + } + + init { + configureGraphComponent() + + add(toolbars, BorderLayout.NORTH) + add(comp, BorderLayout.CENTER) + + fixBounds() + } + + fun scrollToLine(lineNumber: Int) { + lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) -> + scrollCellToVisible(comp, node) + } + } + + fun setMatchToolbarVisible(visible: Boolean) { + matchToolbar.isVisible = visible + } + + fun refresh() { + comp.refresh() + fixBounds() + } + + fun setExprText(text: String) { + matchToolbar.setExprTest(text) + matchToolbar.isVisible = true + } + + fun scrollToNode(node: FlowNode) { + val cell = comp.graph.getChildVertices(comp.graph.defaultParent).asSequence() + .map { it as mxCell } + .find { it.value === node } + ?: return + comp.scrollCellToVisible(cell, true) + } + + fun highlightCells(text: String) { + graph.update { + val vertices = graph.getChildVertices(graph.defaultParent) + var scrolled = false + + for (cell in vertices) { + val flow = (cell as mxCell).value as? FlowNode ?: continue + val texts = listOf( + flow.shortText, + flow.longText, + ) + + if (text.isNotEmpty() && texts.any { text in it.lowercase() }) { + flow.searchHighlight = true + if (!scrolled) { + comp.scrollCellToVisible(cell, true) + comp.zoomTo(1.2, true) + graph.selectionCell = cell + scrolled = true + } + } else { + flow.searchHighlight = false + } + } + } + refresh() + } + + fun onNodeSelected(action: (node: FlowNode?, soft: Boolean) -> Unit) { + fun highlight(e: MouseEvent, soft: Boolean) { + val node = (comp.getCellAt(e.x, e.y) as mxCell?)?.value as? FlowNode + action(node, soft) + comp.refresh() + e.consume() + } + + comp.graphControl.addMouseListener(object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + highlight(e, false) + } + }) + comp.graphControl.addMouseMotionListener(object : MouseAdapter() { + override fun mouseMoved(e: MouseEvent) { + highlight(e, true) + } + }) + } + + private fun configureGraphComponent() { + graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE) { _, _ -> + fixBounds() + } + + graph.isCellsSelectable = false + graph.isCellsEditable = false + comp.isConnectable = false + comp.isPanning = true + comp.setToolTips(true) + comp.viewport.setOpaque(true) + comp.viewport.setBackground(EditorColorsManager.getInstance().globalScheme.defaultBackground) + + comp.graphControl.setOpaque(false) + comp.verticalScrollBar.setUnitIncrement(16) + comp.horizontalScrollBar.setUnitIncrement(16) + } + + private fun fixBounds() { + comp.graphControl.preferredSize = calculateBounds() + comp.graphControl.revalidate() + repaint() + } + + private fun scrollCellToVisible(comp: mxGraphComponent, node: mxCell) { + // Scrolls the cell to the top of the screen if possible + val graph = comp.graph + val state = graph.view.getState(node) ?: return + val cellBounds = state.rectangle + val viewRect = comp.viewport.viewRect + val targetRect = Rectangle( + cellBounds.x, cellBounds.y, + 1, viewRect.height + ) + comp.graphControl.scrollRectToVisible(targetRect) + } + + inner class ViewToolbar : JToolBar() { + private val zoomInButton = JButton("+").apply { + toolTipText = "Zoom In" + addActionListener { + comp.zoomIn() + } + } + private val zoomOutButton = JButton("−").apply { + toolTipText = "Zoom Out" + addActionListener { + comp.zoomOut() + } + } + private val searchField = JTextField() + + init { + isFloatable = false + add(zoomInButton) + add(zoomOutButton) + addSeparator(Dimension(20, 0)) + add(JLabel("Search: ")) + add(searchField) + } + + fun onSearchFieldChanged(action: (String) -> Unit) { + searchField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + action(searchField.text) + } + }) + } + } + + inner class MatchToolbar : JToolBar() { + private val helpLabel = JLabel("Showing matches for:").apply { + border = BorderFactory.createEmptyBorder(0, 6, 0, 0) + } + + private val exprText = JLabel(" ").apply { + font = DiagramStyles.CURRENT_EDITOR_FONT + border = BorderFactory.createEmptyBorder(0, 15, 0, 5) + } + + private val refreshButton = makeButton(AllIcons.Actions.Refresh, "Re-match Expression") + private val clearButton = makeButton(AllIcons.Actions.CloseDarkGrey, "Clear Match Data") + + private val buttonPanel = JPanel().apply { + layout = FlowLayout(FlowLayout.RIGHT, 3, 3) + isOpaque = false + add(refreshButton) + add(clearButton) + } + + init { + isVisible = false + isFloatable = false + layout = BorderLayout() + add(helpLabel, BorderLayout.WEST) + add(exprText, BorderLayout.CENTER) + add(buttonPanel, BorderLayout.EAST) + } + + fun setExprTest(text: String) { + exprText.text = text + exprText.toolTipText = text + } + + fun onTextClicked(action: () -> Unit) { + matchToolbar.exprText.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (e.clickCount == 2) { + action() + } + } + }) + } + + fun onRefresh(action: () -> Unit) { + refreshButton.addActionListener { + action() + } + } + + fun onClear(action: () -> Unit) { + clearButton.addActionListener { + action() + } + } + } +} + +private fun makeButton(icon: Icon, tooltip: String): JButton = + JButton(icon).apply { + toolTipText = tooltip + preferredSize = Dimension(32, 32) + } diff --git a/src/main/kotlin/platform/mixin/expression/gui/GraphUtils.kt b/src/main/kotlin/platform/mixin/expression/gui/GraphUtils.kt new file mode 100644 index 000000000..daddcbf99 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/gui/GraphUtils.kt @@ -0,0 +1,32 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2025 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.gui + +import com.mxgraph.view.mxGraph + +inline fun mxGraph.update(routine: () -> T): T { + model.beginUpdate() + try { + return routine() + } finally { + model.endUpdate() + } +} diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt index fdd9492f9..358edc0f1 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt @@ -90,7 +90,7 @@ class MEFlowWindowService(private val project: Project, private val scope: Corou FlowDiagram.create(project, clazz, method) } ?: return@compute null val container = JPanel(BorderLayout()) - container.add(diagram.panel, BorderLayout.CENTER) + container.add(diagram.ui, BorderLayout.CENTER) val content = ContentFactory.getInstance().createContent(container, getTabName(clazz, method), false) content.putUserData(FLOW_DIAGRAM_KEY, diagram) content diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt index 5a3445226..17f513f30 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt @@ -118,7 +118,7 @@ class MEShowFlowAction : AnAction() { } return methods.map { method -> Resolved(clazz, method) { - it.callbacks.scrollToLine(lineNumber) + it.ui.scrollToLine(lineNumber) } } } From ea57892ab27e6afe15f62a268454a89566a0713a Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 18 Jul 2025 23:19:11 +0100 Subject: [PATCH 12/13] Improvement: Go to *first* best flow match. --- src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt | 2 +- src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 575999950..38cfc2dfe 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -147,7 +147,7 @@ class FlowDiagram( } private fun showBestNode() { - val bestNode = flowGraph.allNodes.values.maxBy { it.matchScore } + val bestNode = flowGraph.orderedNodes.maxBy { it.matchScore } flowGraph.highlightMatches(bestNode, false) ui.scrollToNode(bestNode) } diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt index ada6b889d..082ec91c6 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt @@ -138,6 +138,8 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN private var hardHighlight = false private var hasMatchData = false + val orderedNodes get() = groups.asSequence().flatMap { it.root.dfs() } + operator fun iterator() = groups.iterator() companion object { From fdeaf117a1d3c03fa4aa5a3b28082b4175b139f7 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Fri, 18 Jul 2025 23:52:24 +0100 Subject: [PATCH 13/13] Build: Bump MixinExtras Expressions --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1486c37ad..364328408 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ changelog-plugin = { module = "org.jetbrains.changelog:org.jetbrains.changelog.g coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } mappingIo = "net.fabricmc:mapping-io:0.2.1" -mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.5" +mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.6" jgraphx = "com.github.vlsi.mxgraph:jgraphx:4.2.2" # GrammarKit