User: llamalad7 Date: 27 Jul 25 15:11 Revision: e5ba9d479dc0dbb394da9c7f37a9ffbf48db8a76 Summary: New: Highlight Expression text in Flow Diagram UI with match colours. TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=10118&personal=false Index: src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt (revision da41f3e3b6ee3989ab4cf41d18659da81c8890e7) +++ src/main/kotlin/platform/mixin/expression/gui/DiagramStyles.kt (revision e5ba9d479dc0dbb394da9c7f37a9ffbf48db8a76) @@ -64,21 +64,29 @@ ) 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 Index: src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt (revision da41f3e3b6ee3989ab4cf41d18659da81c8890e7) +++ src/main/kotlin/platform/mixin/expression/gui/FlowDiagram.kt (revision e5ba9d479dc0dbb394da9c7f37a9ffbf48db8a76) @@ -22,10 +22,8 @@ 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.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 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,7 +96,13 @@ flowGraph.highlightMatches(node, soft) ui.refresh() } + + flowGraph.onHighlightChanged { exprText, node -> + scope.launch(Dispatchers.EDT) { + ui.showExpr(exprText, node) - } + } + } + } fun populateMatchStatuses( module: Module, @@ -105,44 +116,45 @@ 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 + 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) + val expression = stringLit.constantStringValue?.let(MEExpressionMatchUtil::createExpression) - ?: return@run null + ?: 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 - ) {} - } + 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.setExprText(expression.src.toString()) - flowGraph.highlightMatches(oldHighlightRoot, false) + flowGraph.highlightMatches(oldHighlightRoot, false) - StringUtil.escapeStringCharacters(expression.src.toString()) - }) - .finishOnUiThread(ModalityState.nonModal()) { exprText -> - exprText ?: return@finishOnUiThread + true + } + if (success) { if (jump) { showBestNode() } + } - ui.refresh() + ui.refresh() - ui.setExprText(exprText) - } + } - .submit(ApplicationManager.getApplication()::executeOnPooledThread) } this.jumpToExpression = { - ReadAction.run { + scope.launch { + readAction { - val target = stringRef.element - if (target is Navigatable && target.isValid && target.canNavigate()) { - target.navigate(true) - } - } - } + val target = stringRef.element + if (target is Navigatable && target.isValid && target.canNavigate()) { + target.navigate(true) + } + } + } + } matchExpression(true) } @@ -161,7 +173,12 @@ } } -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 @@ 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() { Index: src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt (revision da41f3e3b6ee3989ab4cf41d18659da81c8890e7) +++ src/main/kotlin/platform/mixin/expression/gui/FlowDiagramUi.kt (revision e5ba9d479dc0dbb394da9c7f37a9ffbf48db8a76) @@ -22,6 +22,7 @@ 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 @@ 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 @@ add(buttonPanel, BorderLayout.EAST) } - fun setExprTest(text: String) { + fun setExprText(text: String) { exprText.text = text exprText.toolTipText = text } @@ -277,3 +278,54 @@ 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() +} Index: src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt (revision da41f3e3b6ee3989ab4cf41d18659da81c8890e7) +++ src/main/kotlin/platform/mixin/expression/gui/FlowGraph.kt (revision e5ba9d479dc0dbb394da9c7f37a9ffbf48db8a76) @@ -26,9 +26,11 @@ 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 @@ 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 @@ 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 @@ 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 @@ sequenceOf(this) + inputs.asSequence().flatMap { it.dfs() } fun resetMatches() { - matches.clear() + _matches.clear() clearMatchHighlight() } @@ -90,17 +93,17 @@ 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 @@ fun highlightMatches(allNodes: Iterable) { for (node in allNodes) { - node.currentMatchResult = matches.getValue(node.flow) + node.currentMatchResult = _matches.getValue(node.flow) } } } @@ -136,7 +139,9 @@ 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 @@ } fun resetMatches() { - hasMatchData = false + exprText = null highlightRoot = null hardHighlight = false for (node in allNodes.values) { @@ -170,8 +175,8 @@ } } - fun markHasMatchData() { - hasMatchData = true + fun setExprText(exprText: String) { + this.exprText = exprText } fun highlightMatches(root: FlowNode?, soft: Boolean) { @@ -188,7 +193,12 @@ highlightRoot = root clearMatchHighlights() root?.highlightMatches(allNodes.values) + + // Fire listeners + for (listener in highlightListeners) { + listener(exprText!!, root) - } + } + } private fun clearMatchHighlights() { for (node in allNodes.values) { @@ -196,6 +206,10 @@ } } + fun onHighlightChanged(listener: (String, FlowNode?) -> Unit) { + highlightListeners += listener + } + fun shouldShowTooltips() = !hasMatchData || hardHighlight } Index: src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt =================================================================== --- src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt (revision da41f3e3b6ee3989ab4cf41d18659da81c8890e7) +++ src/main/kotlin/platform/mixin/expression/gui/MEFlowWindowService.kt (revision e5ba9d479dc0dbb394da9c7f37a9ffbf48db8a76) @@ -87,7 +87,7 @@ 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)