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"> + + +