User: llamalad7 Date: 19 Jul 25 15:44 Revision: e1b4202f572b247b8741fbc654871a7b48f62476 Summary: Feature/flow graph improvements (#2494) * Fix: Add/fix some FlowStrings. * New: Show expression matching in flow diagrams. * Cleanup: Extract FlowDiagram styles to DiagramStyles. * New: Add refreshing to flow graph matching. * Improvement: Show attempted matches in flow graph tooltips. * Accessibility: Use different line thicknesses for match colours in flow diagrams. * Improvement: Show expression text in flow graph UI and allow jumping back to its source. * Improvement: Display expression source in tooltips using editor font. * Fix: Fix NPE when searching flow graph without a selection. * Improvement: Add popup when there is a choice between multiple target methods. * Cleanup: Use MVC for FlowDiagram. * Improvement: Go to *first* best flow match. * Build: Bump MixinExtras Expressions TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=10105&personal=false Index: gradle/libs.versions.toml =================================================================== --- gradle/libs.versions.toml (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ gradle/libs.versions.toml (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -34,7 +34,7 @@ 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 Index: src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -263,6 +263,8 @@ insns: Iterable, contextType: ExpressionContext.Type, forCompletion: Boolean, + crossinline reportMatchStatus: (FlowValue, Expression, Boolean) -> Unit = { _, _, _ -> }, + crossinline reportPartialMatch: (FlowValue, Expression) -> Unit = { _, _ -> }, callback: (ExpressionMatch) -> Unit ) { for (insn in insns) { @@ -283,8 +285,16 @@ // 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, expr, matched) - } + } + override fun reportPartialMatch(node: FlowValue, expr: Expression) { + reportPartialMatch.invoke(node, expr) + } + } + val flow = flows[insn] ?: continue try { val context = ExpressionContext(pool, sink, targetClass, targetMethod, contextType, forCompletion) Index: src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) +++ src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -0,0 +1,84 @@ +/* + * 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 +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 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, + 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 "3.5", + ) + val PARTIAL_MATCH + get() = mapOf( + mxConstants.STYLE_STROKECOLOR to JBColor.orange.hexString, + mxConstants.STYLE_STROKEWIDTH to "2.5", + ) + val SUCCESS + get() = mapOf( + 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) Index: src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -20,32 +20,28 @@ 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.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.colors.EditorFontType +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.module.Module 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.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.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.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 java.util.concurrent.Callable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.objectweb.asm.tree.ClassNode @@ -55,155 +51,171 @@ 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 ui: FlowDiagramUi, + private val flowGraph: FlowGraph, + private val clazz: ClassNode, 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) + return buildDiagram(flowGraph, clazz, 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) + var matchExpression: ((jump: Boolean) -> Unit) = {} + private set + var jumpToExpression: () -> Unit = {} + private set - val panel: JPanel - val scrollToLine = withContext(Dispatchers.EDT) { - panel = JPanel(BorderLayout()) - displayGraphComponent(graph, panel, calculateBounds, lineNumberNodes) + init { + ui.viewToolbar.onSearchFieldChanged { + ui.highlightCells(it) - } + } - return FlowDiagram(method, panel, scrollToLine) + + ui.matchToolbar.onTextClicked { + jumpToExpression() -} + } -private fun displayGraphComponent( - graph: mxGraph, - panel: JPanel, - calculateBounds: () -> Dimension, - lineNumberNodes: SortedMap -): (Int) -> Unit { - val comp = mxGraphComponent(graph) - fun fixBounds() { - comp.graphControl.preferredSize = calculateBounds() + ui.matchToolbar.onRefresh { + matchExpression(false) - } + } - graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE) { _, _ -> - fixBounds() + ui.matchToolbar.onClear { + clearExpression() - } + } - fixBounds() - configureGraphComponent(comp) - val toolbar = createToolbar(comp, ::fixBounds) - panel.add(toolbar, BorderLayout.NORTH) - panel.add(comp, BorderLayout.CENTER) + ui.onNodeSelected { node, soft -> + flowGraph.highlightMatches(node, soft) + ui.refresh() + } + } - return { lineNumber -> - lineNumberNodes.tailMap(lineNumber).firstEntry()?.let { (_, node) -> - scrollCellToVisible(comp, node) + 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 + ui.setMatchToolbarVisible(false) + flowGraph.resetMatches() + 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 null + 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) + StringUtil.escapeStringCharacters(expression.src.toString()) + }) + .finishOnUiThread(ModalityState.nonModal()) { exprText -> + exprText ?: return@finishOnUiThread + if (jump) { + showBestNode() - } + } + ui.refresh() + ui.setExprText(exprText) -} + } - -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) + .submit(ApplicationManager.getApplication()::executeOnPooledThread) -} + } - -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() + this.jumpToExpression = { + ReadAction.run { + val target = stringRef.element + if (target is Navigatable && target.isValid && target.canNavigate()) { + target.navigate(true) - } + } - 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 -} + } + matchExpression(true) + } -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 + private fun showBestNode() { + val bestNode = flowGraph.orderedNodes.maxBy { it.matchScore } + flowGraph.highlightMatches(bestNode, false) + ui.scrollToNode(bestNode) - } + } - 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 + private fun clearExpression() { + ui.setMatchToolbarVisible(false) + flowGraph.resetMatches() + ui.refresh() + matchExpression = {} + jumpToExpression = {} - } + } - } else { - graph.model.setStyle(cell, null) - } +} + +private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: MethodNode): FlowDiagram { + val graph = MxFlowGraph(flowGraph) + setupStyles(graph) + val groupedCells = addGraphContent(graph, flowGraph) + val lineNumberNodes = sortedMapOf() + val calculateBounds = layOutGraph(graph, groupedCells, lineNumberNodes) + + val ui = withContext(Dispatchers.EDT) { + FlowDiagramUi(graph, calculateBounds, lineNumberNodes) - } + } + return FlowDiagram(ui, flowGraph, clazz, method) - } +} - comp.refresh() - fixBounds() - } - }) - 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 + val lines = mutableListOf() + 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( + prefix = "", + separator = "

", + postfix = "" + ) + } override fun convertValueToString(cell: Any?): String { val flow = (cell as? mxCell)?.value as? FlowNode ?: return super.convertValueToString(cell) return flow.shortText } + + override fun getCellStyle(cell: Any?): MutableMap { + val result = super.getCellStyle(cell).toMutableMap() + val flow = (cell as? mxCell)?.value as? FlowNode ?: return result + 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) { + result += DiagramStyles.SEARCH_HIGHLIGHT + } + return result + } +} private suspend fun addGraphContent( graph: mxGraph, @@ -283,65 +295,8 @@ } 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 + val stylesheet = graph.stylesheet + stylesheet.defaultVertexStyle.putAll(DiagramStyles.DEFAULT_NODE) + stylesheet.defaultEdgeStyle.putAll(DiagramStyles.DEFAULT_EDGE) + stylesheet.putCellStyle(LINE_NUMBER_STYLE, DiagramStyles.LINE_NUMBER) - } +} - - 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/FlowDiagramUi.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) +++ src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -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) + } Index: src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -20,10 +20,13 @@ 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 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 @@ -31,15 +34,88 @@ 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.. { + override fun compareTo(other: FlowMatchResult) = status.compareTo(other.status) + + 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" + FlowMatchStatus.PARTIAL -> "Partially matched $attempted" + FlowMatchStatus.SUCCESS -> "Successfully matched $attempted" + } + } +} + +class FlowNode( + val flow: FlowValue, + project: Project, + clazz: ClassNode, + method: MethodNode, + map: MutableMap +) { + private val matches = + mutableMapOf().withDefault { FlowMatchResult(FlowMatchStatus.IGNORED, null) } + var currentMatchResult: FlowMatchResult? = null + private set + val inputs = (0..= FlowMatchStatus.PARTIAL } + init { + map[flow] = this + } + fun dfs(): Sequence = sequenceOf(this) + inputs.asSequence().flatMap { it.dfs() } + + fun resetMatches() { + matches.clear() + clearMatchHighlight() -} + } + fun clearMatchHighlight() { + currentMatchResult = null + } + + 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, expr: Expression) { + updateMatchStatus(childFlow, FlowMatchResult(FlowMatchStatus.PARTIAL, expr.src.toString())) + } + + private fun updateMatchStatus(childFlow: FlowValue, status: FlowMatchResult) { + matches.compute(childFlow) { _, oldStatus -> + if (oldStatus == null) { + status + } else { + maxOf(oldStatus, status) + } + } + } + + fun highlightMatches(allNodes: Iterable) { + for (node in allNodes) { + node.currentMatchResult = matches.getValue(node.flow) + } + } +} + class FlowGroup(val root: FlowNode, method: MethodNode) : Comparable { private val startIndex = root.dfs() .map { it.flow } @@ -56,26 +132,71 @@ 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) { + var highlightRoot: FlowNode? = null + private set + private var hardHighlight = false + private var hasMatchData = false + + val orderedNodes get() = groups.asSequence().flatMap { it.root.dfs() } + 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() { + 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 + } + 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() + } + } + + fun shouldShowTooltips() = !hasMatchData || hardHighlight +} + 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 eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/expression/gui/FlowStrings.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -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 @@ -39,6 +41,7 @@ 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 +66,9 @@ 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) @@ -77,14 +83,8 @@ 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) { @@ -96,6 +96,7 @@ } return when (cst) { Type.VOID_TYPE -> "null" + is Type -> "${cst.shortName}.class" is String -> "'${cst.escape()}'" is Float -> "${cst}F" is Long -> "${cst}L" @@ -110,7 +111,7 @@ 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 { @@ -128,7 +129,9 @@ location = location.next } val localName = ReadAction.compute<_, Nothing> { + runCatching { - LocalVariables.getLocalVariableAt(project, clazz, method, location, index) + LocalVariables.getLocalVariableAt(project, clazz, method, location, index) + }.getOrNull() }?.name ?: "" val isStore = flow.insn.opcode in Opcodes.ISTORE..Opcodes.ASTORE return localName + if (isStore) " =" else "" @@ -147,17 +150,26 @@ 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})" 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 +188,11 @@ 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" Index: src/main/kotlin/platform/mixin/expression/gui/GraphUtils.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/GraphUtils.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) +++ src/main/kotlin/platform/mixin/expression/gui/GraphUtils.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -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() + } +} Index: src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -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 @@ -46,13 +47,13 @@ @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 +69,9 @@ 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? { @@ -90,7 +90,7 @@ 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 Index: src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/expression/gui/MEShowFlowAction.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -20,15 +20,29 @@ package com.demonwav.mcdev.platform.mixin.expression.gui +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.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 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.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 @@ -43,55 +57,108 @@ 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, lineNumber) = resolve(e) ?: return - project.service().showDiagram(clazz, method, lineNumber) + val results = resolve(e).ifEmpty { return } + + fun navigate(resolved: Resolved) { + project.service().showDiagram(resolved.clazz, resolved.method, resolved.action) - } + } - private fun resolve(e: AnActionEvent): Resolved? { - val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null + 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): 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.find { method -> + val methods = clazz.methods.asSequence().filter { method -> method.instructions.asSequence() .filterIsInstance() .any { it.line == lineNumber } - } ?: return null - return Resolved(clazz, method, lineNumber) - } + } + return methods.map { method -> + Resolved(clazz, method) { + it.ui.scrollToLine(lineNumber) + } + } + } - return resolveMixinMethodString() - ?: resolvePsiMethod() - ?: resolveMethodByLine() + 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 emptySequence() - } + } + val (injectorAnnotation, injector) = + modifierList.annotations.firstNotNullOfOrNull { ann -> + (MixinAnnotationHandler.forMixinAnnotation(ann, project) as? InjectorAnnotationHandler) + ?.let { ann to it } + } ?: 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) + } + } + } - private data class Resolved(val clazz: ClassNode, val method: MethodNode, val line: Int? = null) + 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 = {}) { + override fun toString() = "${clazz.shortName}::${method.name}${shortDescString(method.desc)}" + } +} Index: src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt =================================================================== --- src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/reference/AbstractMethodReference.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -105,7 +105,7 @@ 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 } Index: src/main/kotlin/platform/mixin/util/AsmUtil.kt =================================================================== --- src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision eaec458e29fb695207ce85a72d5472ad83c0020f) +++ src/main/kotlin/platform/mixin/util/AsmUtil.kt (revision e1b4202f572b247b8741fbc654871a7b48f62476) @@ -183,6 +183,12 @@ 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)