User: llamalad7
Date: 02 Jul 25 23:23
Revision: 355921386f735ba13dd22889cafc8cab6b786abb
Summary:
New: MixinExtras Flow Graph
TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=10097&personal=false
Index: build.gradle.kts
===================================================================
--- build.gradle.kts (revision d192c92da162c3a72b1779514c406d173c878b05)
+++ build.gradle.kts (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -90,6 +90,7 @@
exclude(group = "org.ow2.asm", module = "asm-debug-all")
}
testLibs(libs.mixinExtras.common)
+ implementation(libs.jgraphx)
implementation(libs.mappingIo)
implementation(libs.bundles.asm)
Index: gradle/libs.versions.toml
===================================================================
--- gradle/libs.versions.toml (revision d192c92da162c3a72b1779514c406d173c878b05)
+++ gradle/libs.versions.toml (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -34,7 +34,8 @@
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.4"
+mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.5"
+jgraphx = "com.github.vlsi.mxgraph:jgraphx:4.2.2"
# GrammarKit
jflex-lib = "org.jetbrains.idea:jflex:1.7.0-b7f882a"
Index: src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt
===================================================================
--- src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
+++ src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -0,0 +1,347 @@
+/*
+ * 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.application.EDT
+import com.intellij.openapi.editor.colors.EditorColorsManager
+import com.intellij.openapi.editor.colors.EditorFontType
+import com.intellij.openapi.progress.checkCanceled
+import com.intellij.openapi.project.Project
+import com.intellij.util.ui.JBUI
+import com.intellij.util.ui.UIUtil
+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.util.SortedMap
+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
+import org.objectweb.asm.tree.MethodNode
+
+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"
+
+class FlowDiagram(
+ val method: MethodNode,
+ val panel: JPanel,
+ val scrollToLine: (Int) -> Unit,
+) {
+ 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)
+ }
+ }
+}
+
+private suspend fun buildPanel(flowGraph: FlowGraph, method: MethodNode): FlowDiagram {
+ val graph = MxFlowGraph()
+ setupStyles(graph)
+ val groupedCells = addGraphContent(graph, flowGraph)
+ val lineNumberNodes = sortedMapOf()
+ val calculateBounds = layOutGraph(graph, groupedCells, lineNumberNodes)
+
+ val panel: JPanel
+ val scrollToLine = withContext(Dispatchers.EDT) {
+ panel = JPanel(BorderLayout())
+ displayGraphComponent(graph, panel, calculateBounds, lineNumberNodes)
+ }
+ return FlowDiagram(method, panel, scrollToLine)
+}
+
+private fun displayGraphComponent(
+ graph: mxGraph,
+ panel: JPanel,
+ calculateBounds: () -> Dimension,
+ lineNumberNodes: SortedMap
+): (Int) -> Unit {
+ val comp = mxGraphComponent(graph)
+ fun fixBounds() {
+ comp.graphControl.preferredSize = calculateBounds()
+ }
+
+ graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE) { _, _ ->
+ fixBounds()
+ }
+ fixBounds()
+ configureGraphComponent(comp)
+
+ val toolbar = createToolbar(comp, ::fixBounds)
+ panel.add(toolbar, BorderLayout.NORTH)
+ panel.add(comp, BorderLayout.CENTER)
+
+ return { lineNumber ->
+ lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) ->
+ scrollCellToVisible(comp, node)
+ }
+ }
+}
+
+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 createToolbar(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 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) {
+ cell as mxCell
+ if (cell.style == LINE_NUMBER_STYLE) {
+ 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))
+ if (!scrolled) {
+ comp.scrollCellToVisible(cell, true)
+ comp.zoomTo(1.2, true)
+ graph.selectionCell = cell
+ scrolled = true
+ }
+ } else {
+ graph.model.setStyle(cell, null)
+ }
+ }
+ }
+ comp.refresh()
+ fixBounds()
+ }
+ })
+ return searchField
+}
+
+private class MxFlowGraph : mxGraph() {
+ override fun getToolTipForCell(cell: Any?): String {
+ val flow = (cell as? mxCell)?.value as? FlowNode ?: return super.getToolTipForCell(cell)
+ return flow.longText
+ }
+
+ override fun convertValueToString(cell: Any?): String {
+ val flow = (cell as? mxCell)?.value as? FlowNode ?: return super.convertValueToString(cell)
+ return flow.shortText
+ }
+}
+
+private suspend fun addGraphContent(
+ graph: mxGraph,
+ flowGraph: FlowGraph
+): SortedMap> {
+ val groupedCells = sortedMapOf>()
+ graph.update {
+ fun addFlow(flow: FlowNode, parent: mxCell?, out: (mxCell) -> Unit) {
+ val node = graph.insertVertex(null, null, flow, 0.0, 0.0, 0.0, 0.0) as mxCell
+ graph.updateCellSize(node, true)
+ if (parent != null) {
+ out(graph.insertEdge(null, null, null, node, parent) as mxCell)
+ }
+ for (input in flow.inputs) {
+ addFlow(input, node, out)
+ }
+ out(node)
+ }
+
+ for (group in flowGraph) {
+ @Suppress("UnstableApiUsage")
+ checkCanceled()
+ val cells = mutableListOf()
+ addFlow(group.root, null, cells::add)
+ groupedCells[group] = cells
+ }
+ }
+ return groupedCells
+}
+
+private suspend fun layOutGraph(
+ graph: mxGraph,
+ groupedCells: SortedMap>,
+ lineNumberNodes: SortedMap
+): () -> Dimension {
+ val layout = mxHierarchicalLayout(graph)
+ var lastBounds = mxRectangle(0.0, 0.0, 0.0, 0.0)
+ var maxX = 0.0
+ var maxY = 0.0
+ var lastLine: Int? = null
+ for ((group, list) in groupedCells) {
+ @Suppress("UnstableApiUsage")
+ checkCanceled()
+
+ val (targetLeft, targetTop) = if (group.lineNumber == lastLine) {
+ (lastBounds.x + lastBounds.width + INTRA_GROUP_SPACING) to (lastBounds.y)
+ } else {
+ val label = graph.insertVertex(
+ null, null,
+ "Line ${group.lineNumber}:",
+ OUTER_PADDING / 2,
+ maxY + INTER_GROUP_SPACING / 2,
+ 0.0, 0.0,
+ LINE_NUMBER_STYLE
+ ) as mxCell
+ lineNumberNodes[group.lineNumber] = label
+ graph.updateCellSize(label, true)
+ graph.moveCells(arrayOf(label), 0.0, -graph.view.getState(label).height / 2)
+ (OUTER_PADDING) to (maxY + INTER_GROUP_SPACING)
+ }
+ layout.execute(graph.getDefaultParent(), list)
+ val cells = list.toTypedArray()
+ val bounds = graph.view.getBounds(cells)
+ graph.moveCells(cells, -bounds.x + targetLeft, -bounds.y + targetTop)
+ lastBounds = mxRectangle(targetLeft, targetTop, bounds.width, bounds.height)
+ maxX = maxOf(maxX, lastBounds.x + lastBounds.width)
+ maxY = maxOf(maxY, lastBounds.y + lastBounds.height)
+ lastLine = group.lineNumber
+ }
+
+ return {
+ Dimension(
+ ((maxX + OUTER_PADDING) * graph.view.scale).toInt(),
+ ((maxY + OUTER_PADDING) * graph.view.scale).toInt()
+ )
+ }
+}
+
+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",
+ )
+ )
+}
+
+private fun configureGraphComponent(comp: mxGraphComponent) {
+ 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)
+}
+
+private val Color.hexString get() = "#%06X".format(rgb)
+
+private inline fun mxGraph.update(routine: () -> T): T {
+ model.beginUpdate()
+ try {
+ return routine()
+ } finally {
+ model.endUpdate()
+ }
+}
Index: src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt
===================================================================
--- src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
+++ src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -0,0 +1,81 @@
+/*
+ * 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.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.llamalad7.mixinextras.expression.impl.flow.FlowValue
+import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander
+import java.util.SortedSet
+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.. =
+ sequenceOf(this) + inputs.asSequence().flatMap { it.dfs() }
+}
+
+class FlowGroup(val root: FlowNode, method: MethodNode) : Comparable {
+ private val startIndex = root.dfs()
+ .map { it.flow }
+ .filterNot { it.isComplex }
+ .minOf {
+ method.instructions.indexOf(InsnExpander.getRepresentative(it))
+ }
+
+ val lineNumber =
+ generateSequence(method.instructions.get(startIndex)) { it.previous }
+ .filterIsInstance()
+ .firstOrNull()?.line ?: -1
+
+ override fun compareTo(other: FlowGroup) = compareValuesBy(this, other, { it.lineNumber }, { it.startIndex })
+}
+
+class FlowGraph(val groups: SortedSet) {
+ 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 groups = sortedSetOf()
+ for (flow in flows) {
+ if (!flow.isRoot) {
+ continue
+ }
+ @Suppress("UnstableApiUsage")
+ checkCanceled()
+
+ val node = FlowNode(flow, project, clazz, method)
+ groups.add(FlowGroup(node, method))
+ }
+ return FlowGraph(groups)
+ }
+ }
+}
+
+private val FlowValue.isRoot get() = next.isEmpty()
Index: src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt
===================================================================
--- src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
+++ src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -0,0 +1,205 @@
+/*
+ * 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.demonwav.mcdev.platform.mixin.util.LocalVariables
+import com.demonwav.mcdev.platform.mixin.util.textify
+import com.intellij.openapi.application.ReadAction
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.text.StringUtil
+import com.llamalad7.mixinextras.expression.impl.flow.FlowValue
+import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander
+import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.InstantiationInfo
+import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.LMFInfo
+import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.MethodCallType
+import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.StringConcatInfo
+import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils
+import com.llamalad7.mixinextras.expression.impl.utils.FlowDecorations
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+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.VarInsnNode
+
+fun FlowValue.shortString(project: Project, clazz: ClassNode, method: MethodNode): String {
+ if (isComplex) {
+ return "Complex Flow (type ${type.shortName})"
+ }
+ ExpressionASMUtils.getConstant(insn)?.let { return constantString(it) }
+ ExpressionASMUtils.getCastType(insn)?.let { return castString(it) }
+ newArrayString(this)?.let { return it }
+ getDecoration(FlowDecorations.METHOD_CALL_TYPE)?.let { methodCallType ->
+ return methodString(insn as MethodInsnNode, methodCallType)
+ }
+ getDecoration(FlowDecorations.INSTANTIATION_INFO)?.let { info ->
+ return instantiationString(info)
+ }
+ getDecoration(FlowDecorations.STRING_CONCAT_INFO)?.let { _ ->
+ return "+"
+ }
+ getDecoration(FlowDecorations.LMF_INFO)?.let { info ->
+ return lmfString(info)
+ }
+ getDecoration(FlowDecorations.COMPLEX_COMPARISON_JUMP)?.let { jump ->
+ return complexCmpString(insn.opcode, jump.insn.opcode)
+ }
+ return when (val insn = insn) {
+ is FieldInsnNode -> fieldString(insn)
+ is VarInsnNode -> varString(this, insn.`var`, project, clazz, method)
+ else -> opcodeString(insn.opcode) ?: longString()
+ }
+}
+
+fun FlowValue.longString(): String =
+ if (isComplex) {
+ "Complex Flow (type ${type.className})"
+ } else {
+ 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) {
+ 0 -> "0 or false"
+ 1 -> "1 or true"
+ in Char.MIN_VALUE.code..Char.MAX_VALUE.code -> "$cst or '${cst.toChar().toString().escape()}'"
+ else -> cst.toString()
+ }
+ }
+ return when (cst) {
+ Type.VOID_TYPE -> "null"
+ is String -> "'${cst.escape()}'"
+ is Float -> "${cst}F"
+ is Long -> "${cst}L"
+ else -> cst.toString()
+ }
+}
+
+private fun instantiationString(info: InstantiationInfo): String {
+ val start = "new ${info.type.shortName}"
+ val insn = info.initCall
+ if (insn.isComplex) {
+ return start
+ }
+ val call = insn.insn as? MethodInsnNode ?: return start
+ return start + shortParams(call.desc)
+}
+
+private fun lmfString(info: LMFInfo): String {
+ val handle = info.impl
+ return when (info.type!!) {
+ LMFInfo.Type.FREE_METHOD -> "${shortOwner(handle.owner)}::${handle.name}"
+ LMFInfo.Type.BOUND_METHOD -> "::${handle.name}"
+ LMFInfo.Type.INSTANTIATION -> "${shortOwner(handle.owner)}::new"
+ }
+}
+
+private fun varString(flow: FlowValue, index: Int, project: Project, clazz: ClassNode, method: MethodNode): String {
+ var location = InsnExpander.getRepresentative(flow)
+ if (location.opcode in Opcodes.ISTORE..Opcodes.ASTORE) {
+ location = location.next
+ }
+ val localName = ReadAction.compute<_, Nothing> {
+ LocalVariables.getLocalVariableAt(project, clazz, method, location, index)
+ }?.name ?: ""
+ val isStore = flow.insn.opcode in Opcodes.ISTORE..Opcodes.ASTORE
+ return localName + if (isStore) " =" else ""
+}
+
+private fun fieldString(insn: FieldInsnNode): String {
+ val isStatic = insn.opcode in Opcodes.GETSTATIC..Opcodes.PUTSTATIC
+ val isWrite = insn.opcode == Opcodes.PUTFIELD || insn.opcode == Opcodes.PUTSTATIC
+ val owner = if (isStatic) shortOwner(insn.owner) else ""
+ return "$owner.${insn.name}" + if (isWrite) " =" else ""
+}
+
+private fun methodString(insn: MethodInsnNode, methodCallType: MethodCallType): String {
+ val owner = when (methodCallType) {
+ MethodCallType.NORMAL -> ""
+ MethodCallType.SUPER -> "super"
+ MethodCallType.STATIC -> shortOwner(insn.owner)
+ }
+ return "$owner.${insn.name}${shortParams(insn.desc)}"
+}
+
+private fun castString(type: Type): String = "(${type.shortName})"
+
+private fun complexCmpString(opcode: Int, jumpOpcode: Int): String {
+ val isG = opcode == Opcodes.FCMPG || opcode == Opcodes.DCMPG
+ return when (jumpOpcode) {
+ Opcodes.IFEQ, Opcodes.IFNE -> "== or !="
+ Opcodes.IFLT, Opcodes.IFGE -> if (isG) "<" else ">="
+ Opcodes.IFGT, Opcodes.IFLE -> if (isG) "<=" else ">"
+ else -> "Unknown jump"
+ }
+}
+
+private fun newArrayString(flow: FlowValue): String? {
+ val insn = flow.insn
+ val type = when (insn.opcode) {
+ Opcodes.NEWARRAY, Opcodes.ANEWARRAY -> ExpressionASMUtils.getUnaryType(insn)
+ Opcodes.MULTIANEWARRAY -> ExpressionASMUtils.getNaryType(insn)
+ else -> return null
+ }
+ val prefix = "new ${type.shortName}"
+ if (!flow.hasDecoration(FlowDecorations.ARRAY_CREATION_INFO)) {
+ return prefix
+ }
+ return "$prefix{ }"
+}
+
+private fun opcodeString(opcode: Int) = when (opcode) {
+ Opcodes.ATHROW -> "throw"
+ in Opcodes.IRETURN..Opcodes.RETURN -> "return"
+ in Opcodes.IADD..Opcodes.DADD -> "+"
+ in Opcodes.ISUB..Opcodes.DSUB -> "-"
+ in Opcodes.IMUL..Opcodes.DMUL -> "*"
+ in Opcodes.IDIV..Opcodes.DDIV -> "/"
+ in Opcodes.IREM..Opcodes.DREM -> "%"
+ in Opcodes.INEG..Opcodes.DNEG -> "-"
+ Opcodes.ISHL, Opcodes.LSHL -> "<<"
+ Opcodes.ISHR, Opcodes.LSHR -> ">>"
+ Opcodes.IUSHR, Opcodes.LUSHR -> ">>>"
+ Opcodes.IAND, Opcodes.LAND -> "&"
+ Opcodes.IOR, Opcodes.LOR -> "|"
+ Opcodes.IXOR, Opcodes.LXOR -> "^"
+ Opcodes.IF_ACMPEQ, Opcodes.IF_ICMPEQ, Opcodes.IF_ACMPNE, Opcodes.IF_ICMPNE -> "== or !="
+ Opcodes.IF_ICMPLT, Opcodes.IF_ICMPGE -> "< or >="
+ Opcodes.IF_ICMPLE, Opcodes.IF_ICMPGT -> "<= or >"
+ Opcodes.ARRAYLENGTH -> ".length"
+ in Opcodes.IALOAD..Opcodes.SALOAD -> "[]"
+ in Opcodes.IASTORE..Opcodes.SASTORE -> "[] ="
+ else -> null
+}
+
+private fun String.escape() =
+ StringUtil.escapeStringCharacters(this)
+ .replace("'", "\\\\'")
Index: src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt
===================================================================
--- src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
+++ src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -0,0 +1,102 @@
+/*
+ * 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.application.EDT
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.Messages
+import com.intellij.openapi.util.Key
+import com.intellij.openapi.wm.ToolWindow
+import com.intellij.openapi.wm.ToolWindowAnchor
+import com.intellij.openapi.wm.ToolWindowManager
+import com.intellij.platform.ide.progress.withBackgroundProgress
+import com.intellij.ui.content.Content
+import com.intellij.ui.content.ContentFactory
+import java.awt.BorderLayout
+import javax.swing.JPanel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.objectweb.asm.Type
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.MethodNode
+
+private const val TOOL_WINDOW_ID = "MixinExtras Flow Diagram"
+private val FLOW_DIAGRAM_KEY = Key.create("${MEFlowWindowService::class.java.name}.flowDiagram")
+
+@Service(Service.Level.PROJECT)
+class MEFlowWindowService(private val project: Project, private val scope: CoroutineScope) {
+ fun showDiagram(clazz: ClassNode, method: MethodNode, lineNumber: Int?) {
+ scope.launch(Dispatchers.EDT) {
+ showDiagramImpl(clazz, method, lineNumber)
+ }
+ }
+
+ private suspend fun showDiagramImpl(clazz: ClassNode, method: MethodNode, lineNumber: Int?) {
+ val toolWindowManager = ToolWindowManager.getInstance(project)
+ var toolWindow = toolWindowManager.getToolWindow(TOOL_WINDOW_ID)
+
+ if (toolWindow == null) {
+ toolWindow = toolWindowManager.registerToolWindow(TOOL_WINDOW_ID) {
+ canCloseContent = true
+ anchor = ToolWindowAnchor.RIGHT
+ hideOnEmptyContent = true
+ }
+ }
+
+ val content = chooseContent(toolWindow, clazz, method) ?: return Messages.showErrorDialog(
+ project, "Failed to create flow diagram", "Error"
+ )
+ toolWindow.contentManager.setSelectedContent(content)
+ if (lineNumber != null) {
+ content.getUserData(FLOW_DIAGRAM_KEY)?.scrollToLine?.invoke(lineNumber)
+ }
+ toolWindow.activate(null)
+ }
+
+ private suspend fun chooseContent(toolWindow: ToolWindow, clazz: ClassNode, method: MethodNode): Content? {
+ val contentManager = toolWindow.contentManager
+ val existing = contentManager.contents.find { it.getUserData(FLOW_DIAGRAM_KEY)?.method === method }
+ existing?.let { return it }
+ val content = createContent(clazz, method) ?: return null
+ content.isCloseable = true
+ contentManager.addContent(content)
+ return content
+ }
+
+ private suspend fun createContent(clazz: ClassNode, method: MethodNode): Content? =
+ withBackgroundProgress(project, "Creating Flow Diagram") compute@{
+ val diagram = withContext(Dispatchers.Default) {
+ FlowDiagram.create(project, clazz, method)
+ } ?: return@compute null
+ val container = JPanel(BorderLayout())
+ container.add(diagram.panel, BorderLayout.CENTER)
+ val content = ContentFactory.getInstance().createContent(container, getTabName(clazz, method), false)
+ content.putUserData(FLOW_DIAGRAM_KEY, diagram)
+ content
+ }
+
+ private fun getTabName(clazz: ClassNode, method: MethodNode): String {
+ return Type.getObjectType(clazz.name).shortName + "::" + method.name
+ }
+}
Index: src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt
===================================================================
--- src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
+++ src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -0,0 +1,97 @@
+/*
+ * 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.demonwav.mcdev.platform.mixin.reference.MethodReference
+import com.demonwav.mcdev.platform.mixin.util.findClassNodeByPsiClass
+import com.demonwav.mcdev.util.descriptor
+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.components.service
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiIdentifier
+import com.intellij.psi.PsiLiteralExpression
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.util.parentOfType
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.LineNumberNode
+import org.objectweb.asm.tree.MethodNode
+
+class MEShowFlowAction : AnAction() {
+
+ override fun getActionUpdateThread() = ActionUpdateThread.BGT
+
+ override fun update(e: AnActionEvent) {
+ e.presentation.isEnabledAndVisible = resolve(e) != null
+ }
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ val (clazz, method, lineNumber) = resolve(e) ?: return
+ project.service().showDiagram(clazz, method, lineNumber)
+ }
+
+ private fun resolve(e: AnActionEvent): Resolved? {
+ val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null
+ if (file.language != JavaLanguage.INSTANCE) {
+ return null
+ }
+ val caret = e.getData(CommonDataKeys.CARET) ?: return null
+ val element = file.findElementAt(caret.offset) ?: return null
+ val psiClass = element.parentOfType() ?: return null
+
+ fun resolveMixinMethodString(): Resolved? {
+ val string = element.parentOfType() ?: return null
+ return MethodReference.resolveIfUnique(string)?.let { (clazz, method) ->
+ Resolved(clazz, method)
+ }
+ }
+
+ 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 resolveMethodByLine(): Resolved? {
+ val clazz = findClassNodeByPsiClass(psiClass) ?: return null
+ val lineNumber = caret.logicalPosition.line + 1
+ val method = clazz.methods.find { method ->
+ method.instructions.asSequence()
+ .filterIsInstance()
+ .any { it.line == lineNumber }
+ } ?: return null
+ return Resolved(clazz, method, lineNumber)
+ }
+
+ return resolveMixinMethodString()
+ ?: resolvePsiMethod()
+ ?: resolveMethodByLine()
+ }
+
+ private data class Resolved(val clazz: ClassNode, val method: MethodNode, val line: Int? = null)
+}
Index: src/main/kotlin/platform/mixin/util/LocalVariables.kt
===================================================================
--- src/main/kotlin/platform/mixin/util/LocalVariables.kt (revision d192c92da162c3a72b1779514c406d173c878b05)
+++ src/main/kotlin/platform/mixin/util/LocalVariables.kt (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -692,7 +692,7 @@
return size.coerceAtLeast(initialFrameSize)
}
- private fun getLocalVariableAt(
+ fun getLocalVariableAt(
project: Project,
classNode: ClassNode,
method: MethodNode,
Index: src/main/resources/META-INF/plugin.xml
===================================================================
--- src/main/resources/META-INF/plugin.xml (revision d192c92da162c3a72b1779514c406d173c878b05)
+++ src/main/resources/META-INF/plugin.xml (revision 355921386f735ba13dd22889cafc8cab6b786abb)
@@ -1551,6 +1551,11 @@
description="Generate an accessor for the selected member">
+
+
+