From f97fb793177144b710ae28dcf2b5749142ec9d2f Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 27 Jul 2025 16:11:34 +0100 Subject: [PATCH] New: Highlight Expression text in Flow Diagram UI with match colours. --- .../mixin/expression/gui/DiagramStyles.kt | 16 +++- .../mixin/expression/gui/FlowDiagram.kt | 89 +++++++++++-------- .../mixin/expression/gui/FlowDiagramUi.kt | 58 +++++++++++- .../mixin/expression/gui/FlowGraph.kt | 38 +++++--- .../expression/gui/MEFlowWindowService.kt | 2 +- 5 files changed, 147 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt index 657198b36..721088bd0 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt @@ -64,21 +64,29 @@ object DiagramStyles { ) val FAILED get() = mapOf( - mxConstants.STYLE_STROKECOLOR to JBColor.red.hexString, + mxConstants.STYLE_STROKECOLOR to FlowMatchStatus.FAIL.hexColor, mxConstants.STYLE_STROKEWIDTH to "3.5", ) val PARTIAL_MATCH get() = mapOf( - mxConstants.STYLE_STROKECOLOR to JBColor.orange.hexString, + mxConstants.STYLE_STROKECOLOR to FlowMatchStatus.PARTIAL.hexColor, mxConstants.STYLE_STROKEWIDTH to "2.5", ) val SUCCESS get() = mapOf( - mxConstants.STYLE_STROKECOLOR to JBColor.green.hexString, + mxConstants.STYLE_STROKECOLOR to FlowMatchStatus.SUCCESS.hexColor, 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) +private val Color.hexString get() = "#%06X".format(rgb and 0xFFFFFF) + +val FlowMatchStatus.hexColor + get() = when (this) { + FlowMatchStatus.SUCCESS -> JBColor.green + FlowMatchStatus.PARTIAL -> JBColor.orange + FlowMatchStatus.FAIL -> JBColor.red + FlowMatchStatus.IGNORED -> UIUtil.getLabelForeground() + }.hexString diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt index 38cfc2dfe..9f87abd76 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt @@ -22,10 +22,8 @@ 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.application.ModalityState -import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.readAction import com.intellij.openapi.module.Module import com.intellij.openapi.progress.checkCanceled import com.intellij.openapi.project.Project @@ -41,8 +39,9 @@ import com.mxgraph.util.mxRectangle import com.mxgraph.view.mxGraph import java.awt.Dimension import java.util.SortedMap -import java.util.concurrent.Callable +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -53,15 +52,21 @@ private const val INTRA_GROUP_SPACING = 75 private const val LINE_NUMBER_STYLE = "LINE_NUMBER" class FlowDiagram( + private val scope: CoroutineScope, val ui: FlowDiagramUi, private val flowGraph: FlowGraph, private val clazz: ClassNode, val method: MethodNode, ) { companion object { - suspend fun create(project: Project, clazz: ClassNode, method: MethodNode): FlowDiagram? { + suspend fun create( + project: Project, + scope: CoroutineScope, + clazz: ClassNode, + method: MethodNode + ): FlowDiagram? { val flowGraph = FlowGraph.parse(project, clazz, method) ?: return null - return buildDiagram(flowGraph, clazz, method) + return buildDiagram(scope, flowGraph, clazz, method) } } @@ -91,6 +96,12 @@ class FlowDiagram( flowGraph.highlightMatches(node, soft) ui.refresh() } + + flowGraph.onHighlightChanged { exprText, node -> + scope.launch(Dispatchers.EDT) { + ui.showExpr(exprText, node) + } + } } fun populateMatchStatuses( @@ -105,41 +116,42 @@ class FlowDiagram( 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 - ) {} + scope.launch(Dispatchers.Default) { + val success = readAction run@{ + val stringLit = stringRef.element ?: return@run false + val modifierList = modifierListRef.element ?: return@run false + val expression = stringLit.constantStringValue?.let(MEExpressionMatchUtil::createExpression) + ?: return@run false + 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.setExprText(expression.src.toString()) + flowGraph.highlightMatches(oldHighlightRoot, false) + true } - flowGraph.markHasMatchData() - flowGraph.highlightMatches(oldHighlightRoot, false) - StringUtil.escapeStringCharacters(expression.src.toString()) - }) - .finishOnUiThread(ModalityState.nonModal()) { exprText -> - exprText ?: return@finishOnUiThread + if (success) { if (jump) { showBestNode() } - ui.refresh() - ui.setExprText(exprText) } - .submit(ApplicationManager.getApplication()::executeOnPooledThread) + ui.refresh() + } } this.jumpToExpression = { - ReadAction.run { - val target = stringRef.element - if (target is Navigatable && target.isValid && target.canNavigate()) { - target.navigate(true) + scope.launch { + readAction { + val target = stringRef.element + if (target is Navigatable && target.isValid && target.canNavigate()) { + target.navigate(true) + } } } } @@ -161,7 +173,12 @@ class FlowDiagram( } } -private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: MethodNode): FlowDiagram { +private suspend fun buildDiagram( + scope: CoroutineScope, + flowGraph: FlowGraph, + clazz: ClassNode, + method: MethodNode +): FlowDiagram { val graph = MxFlowGraph(flowGraph) setupStyles(graph) val groupedCells = addGraphContent(graph, flowGraph) @@ -171,7 +188,7 @@ private suspend fun buildDiagram(flowGraph: FlowGraph, clazz: ClassNode, method: val ui = withContext(Dispatchers.EDT) { FlowDiagramUi(graph, calculateBounds, lineNumberNodes) } - return FlowDiagram(ui, flowGraph, clazz, method) + return FlowDiagram(scope, ui, flowGraph, clazz, method) } private class MxFlowGraph(private val flowGraph: FlowGraph) : mxGraph() { diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt index 1f305a2c7..1e3a715d9 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.platform.mixin.expression.gui import com.intellij.icons.AllIcons import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.util.text.StringUtil import com.intellij.ui.DocumentAdapter import com.mxgraph.model.mxCell import com.mxgraph.swing.mxGraphComponent @@ -83,8 +84,8 @@ class FlowDiagramUi( fixBounds() } - fun setExprText(text: String) { - matchToolbar.setExprTest(text) + fun showExpr(text: String, highlightRoot: FlowNode?) { + matchToolbar.setExprText("" + makeExprString(text, highlightRoot) + "") matchToolbar.isVisible = true } @@ -243,7 +244,7 @@ class FlowDiagramUi( add(buttonPanel, BorderLayout.EAST) } - fun setExprTest(text: String) { + fun setExprText(text: String) { exprText.text = text exprText.toolTipText = text } @@ -277,3 +278,54 @@ private fun makeButton(icon: Icon, tooltip: String): JButton = toolTipText = tooltip preferredSize = Dimension(32, 32) } + +private sealed class HighlightChange : Comparable { + abstract val pos: Int + + data class Start(override val pos: Int, val length: Int, val status: FlowMatchStatus) : HighlightChange() + data class End(override val pos: Int) : HighlightChange() + + override fun compareTo(other: HighlightChange): Int = + compareValuesBy( + this, other, + { it.pos }, + { if (it is Start) 1 else -1 }, + { -((it as? Start)?.length ?: 0) }, + ) +} + +private fun makeExprString(text: String, highlightRoot: FlowNode?): String { + fun escape(str: String) = StringUtil.escapeXmlEntities(StringUtil.escapeStringCharacters(str)) + + if (highlightRoot == null) { + return escape(text) + } + + val changes = mutableListOf() + for ((status, src) in highlightRoot.matches) { + if (src == null) { + continue + } + changes.add(HighlightChange.Start(src.startIndex, src.endIndex - src.startIndex, status)) + changes.add(HighlightChange.End(src.endIndex + 1)) + } + changes.sort() + + val result = StringBuilder() + var pos = 0 + for (change in changes) { + result.append(escape(text.substring(pos, change.pos))) + pos = change.pos + when (change) { + is HighlightChange.Start -> { + result.append("") + } + is HighlightChange.End -> { + result.append("") + } + } + } + result.append(escape(text.substring(pos))) + + return result.toString() +} diff --git a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt index 082ec91c6..e85651a1f 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt @@ -26,9 +26,11 @@ 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.ExpressionSource 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.Collections import java.util.SortedSet import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.LineNumberNode @@ -38,7 +40,7 @@ enum class FlowMatchStatus { IGNORED, FAIL, PARTIAL, SUCCESS } -data class FlowMatchResult(val status: FlowMatchStatus, val attempted: String?) : Comparable { +data class FlowMatchResult(val status: FlowMatchStatus, val attempted: ExpressionSource?) : Comparable { override fun compareTo(other: FlowMatchResult) = status.compareTo(other.status) fun toString(prefix: String, suffix: String, transform: (String) -> String): String { @@ -59,7 +61,7 @@ class FlowNode( method: MethodNode, map: MutableMap ) { - private val matches = + private val _matches = mutableMapOf().withDefault { FlowMatchResult(FlowMatchStatus.IGNORED, null) } var currentMatchResult: FlowMatchResult? = null private set @@ -67,7 +69,8 @@ class FlowNode( val shortText = flow.shortString(project, clazz, method) val longText = flow.longString() var searchHighlight = false - val matchScore get() = matches.values.count { it.status >= FlowMatchStatus.PARTIAL } + val matchScore get() = _matches.values.count { it.status >= FlowMatchStatus.PARTIAL } + val matches: Collection get() = Collections.unmodifiableCollection(_matches.values) init { map[flow] = this @@ -77,7 +80,7 @@ class FlowNode( sequenceOf(this) + inputs.asSequence().flatMap { it.dfs() } fun resetMatches() { - matches.clear() + _matches.clear() clearMatchHighlight() } @@ -90,17 +93,17 @@ class FlowNode( childFlow, FlowMatchResult( if (matched) FlowMatchStatus.SUCCESS else FlowMatchStatus.FAIL, - expr.src.toString() + expr.src ) ) } fun reportPartialMatch(childFlow: FlowValue, expr: Expression) { - updateMatchStatus(childFlow, FlowMatchResult(FlowMatchStatus.PARTIAL, expr.src.toString())) + updateMatchStatus(childFlow, FlowMatchResult(FlowMatchStatus.PARTIAL, expr.src)) } private fun updateMatchStatus(childFlow: FlowValue, status: FlowMatchResult) { - matches.compute(childFlow) { _, oldStatus -> + _matches.compute(childFlow) { _, oldStatus -> if (oldStatus == null) { status } else { @@ -111,7 +114,7 @@ class FlowNode( fun highlightMatches(allNodes: Iterable) { for (node in allNodes) { - node.currentMatchResult = matches.getValue(node.flow) + node.currentMatchResult = _matches.getValue(node.flow) } } } @@ -136,7 +139,9 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN var highlightRoot: FlowNode? = null private set private var hardHighlight = false - private var hasMatchData = false + private val highlightListeners = mutableListOf<(String, FlowNode?) -> Unit>() + private var exprText: String? = null + private val hasMatchData get() = exprText != null val orderedNodes get() = groups.asSequence().flatMap { it.root.dfs() } @@ -162,7 +167,7 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN } fun resetMatches() { - hasMatchData = false + exprText = null highlightRoot = null hardHighlight = false for (node in allNodes.values) { @@ -170,8 +175,8 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN } } - fun markHasMatchData() { - hasMatchData = true + fun setExprText(exprText: String) { + this.exprText = exprText } fun highlightMatches(root: FlowNode?, soft: Boolean) { @@ -188,6 +193,11 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN highlightRoot = root clearMatchHighlights() root?.highlightMatches(allNodes.values) + + // Fire listeners + for (listener in highlightListeners) { + listener(exprText!!, root) + } } private fun clearMatchHighlights() { @@ -196,6 +206,10 @@ class FlowGraph(val groups: SortedSet, val flowMap: FlowMap, val allN } } + fun onHighlightChanged(listener: (String, FlowNode?) -> Unit) { + highlightListeners += listener + } + fun shouldShowTooltips() = !hasMatchData || hardHighlight } diff --git a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt index 358edc0f1..4c7671921 100644 --- a/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt +++ b/src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt @@ -87,7 +87,7 @@ class MEFlowWindowService(private val project: Project, private val scope: Corou 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) + FlowDiagram.create(project, scope, clazz, method) } ?: return@compute null val container = JPanel(BorderLayout()) container.add(diagram.ui, BorderLayout.CENTER)