Skip to content

Commit 260390e

Browse files
committed
Add code vision to show how many mixins target a class
1 parent db033d6 commit 260390e

File tree

6 files changed

+254
-13
lines changed

6 files changed

+254
-13
lines changed

src/main/kotlin/platform/mixin/action/FindMixinsAction.kt

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ import com.intellij.openapi.actionSystem.CommonDataKeys.CARET
3333
import com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR
3434
import com.intellij.openapi.actionSystem.CommonDataKeys.PROJECT
3535
import com.intellij.openapi.actionSystem.CommonDataKeys.PSI_FILE
36+
import com.intellij.openapi.application.ApplicationManager
3637
import com.intellij.openapi.application.runReadAction
38+
import com.intellij.openapi.editor.Editor
3739
import com.intellij.openapi.progress.ProgressIndicator
3840
import com.intellij.openapi.progress.runBackgroundableTask
3941
import com.intellij.openapi.project.Project
@@ -42,6 +44,7 @@ import com.intellij.openapi.wm.ToolWindowFactory
4244
import com.intellij.openapi.wm.ToolWindowManager
4345
import com.intellij.psi.JavaPsiFacade
4446
import com.intellij.psi.PsiClass
47+
import com.intellij.psi.PsiFile
4548
import com.intellij.psi.search.GlobalSearchScope
4649
import com.intellij.psi.search.searches.AnnotatedElementsSearch
4750
import com.intellij.psi.util.PsiModificationTracker
@@ -87,27 +90,26 @@ class FindMixinsAction : AnAction() {
8790
classes
8891
}
8992
}
90-
}
91-
92-
override fun actionPerformed(e: AnActionEvent) {
93-
val project = e.getData(PROJECT) ?: return
94-
val file = e.getData(PSI_FILE) ?: return
95-
val caret = e.getData(CARET) ?: return
96-
val editor = e.getData(EDITOR) ?: return
9793

98-
val element = file.findElementAt(caret.offset) ?: return
99-
val classOfElement = element.findReferencedClass() ?: return
94+
fun openFindMixinsUI(
95+
project: Project,
96+
editor: Editor,
97+
file: PsiFile,
98+
targetClass: PsiClass,
99+
filter: (PsiClass) -> Boolean = { true }
100+
) {
101+
ApplicationManager.getApplication().assertIsDispatchThread()
100102

101-
invokeLater {
102103
runBackgroundableTask("Searching for Mixins", project, true) run@{ indicator ->
103104
indicator.isIndeterminate = true
104105

105106
val classes = runReadAction {
106-
if (!classOfElement.isValid) {
107+
if (!targetClass.isValid) {
107108
return@runReadAction null
108109
}
109110

110-
val classes = findMixins(classOfElement, project, indicator) ?: return@runReadAction null
111+
val classes = findMixins(targetClass, project, indicator)?.filter(filter)
112+
?: return@runReadAction null
111113

112114
when (classes.size) {
113115
0 -> null
@@ -126,7 +128,7 @@ class FindMixinsAction : AnAction() {
126128
val window = twManager.getToolWindow(TOOL_WINDOW_ID)!!
127129
val component = FindMixinsComponent(classes)
128130
val content = ContentFactory.getInstance().createContent(component.panel, null, false)
129-
content.displayName = classOfElement.qualifiedName ?: classOfElement.name
131+
content.displayName = targetClass.qualifiedName ?: targetClass.name
130132
window.contentManager.addContent(content)
131133

132134
window.activate(null)
@@ -135,4 +137,18 @@ class FindMixinsAction : AnAction() {
135137
}
136138
}
137139
}
140+
141+
override fun actionPerformed(e: AnActionEvent) {
142+
val project = e.getData(PROJECT) ?: return
143+
val file = e.getData(PSI_FILE) ?: return
144+
val caret = e.getData(CARET) ?: return
145+
val editor = e.getData(EDITOR) ?: return
146+
147+
val element = file.findElementAt(caret.offset) ?: return
148+
val classOfElement = element.findReferencedClass() ?: return
149+
150+
invokeLater {
151+
openFindMixinsUI(project, editor, file, classOfElement)
152+
}
153+
}
138154
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.mixin.insight.target
22+
23+
import com.intellij.codeInsight.codeVision.CodeVisionEntry
24+
import com.intellij.codeInsight.codeVision.CodeVisionHost
25+
import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering
26+
import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry
27+
import com.intellij.codeInsight.hints.InlayHintsUtils
28+
import com.intellij.codeInsight.hints.codeVision.CodeVisionProviderBase
29+
import com.intellij.codeInsight.hints.settings.language.isInlaySettingsEditor
30+
import com.intellij.lang.java.JavaLanguage
31+
import com.intellij.openapi.application.ApplicationManager
32+
import com.intellij.openapi.editor.Editor
33+
import com.intellij.openapi.util.TextRange
34+
import com.intellij.psi.PsiElement
35+
import com.intellij.psi.PsiFile
36+
import com.intellij.psi.SmartPointerManager
37+
import com.intellij.psi.SyntaxTraverser
38+
import java.awt.event.MouseEvent
39+
40+
abstract class AbstractMixinTargetCodeVisionProvider : CodeVisionProviderBase() {
41+
override val relativeOrderings: List<CodeVisionRelativeOrdering>
42+
get() = listOf(
43+
CodeVisionRelativeOrdering.CodeVisionRelativeOrderingBefore("java.inheritors"),
44+
CodeVisionRelativeOrdering.CodeVisionRelativeOrderingBefore("java.references"),
45+
CodeVisionRelativeOrdering.CodeVisionRelativeOrderingBefore("vcs.code.vision"),
46+
)
47+
48+
override fun computeForEditor(editor: Editor, file: PsiFile): List<Pair<TextRange, CodeVisionEntry>> {
49+
// copied from superclass implementation, except without the check for libraries
50+
51+
if (file.project.isDefault) {
52+
return emptyList()
53+
}
54+
if (!acceptsFile(file)) {
55+
return emptyList()
56+
}
57+
58+
// we want to let this provider work only in tests dedicated for code vision, otherwise they harm performance
59+
if (ApplicationManager.getApplication().isUnitTestMode && !CodeVisionHost.isCodeLensTest()) {
60+
return emptyList()
61+
}
62+
63+
val lenses = ArrayList<Pair<TextRange, CodeVisionEntry>>()
64+
val traverser = SyntaxTraverser.psiTraverser(file)
65+
for (element in traverser) {
66+
if (!acceptsElement(element)) {
67+
continue
68+
}
69+
if (!InlayHintsUtils.isFirstInLine(element)) {
70+
continue
71+
}
72+
val hint = getHint(element, file) ?: continue
73+
val handler = ClickHandler(element, hint)
74+
val range = InlayHintsUtils.getTextRangeWithoutLeadingCommentsAndWhitespaces(element)
75+
lenses.add(range to ClickableTextCodeVisionEntry(hint, id, handler))
76+
}
77+
return lenses
78+
}
79+
80+
override fun acceptsFile(file: PsiFile) = file.language == JavaLanguage.INSTANCE
81+
82+
private inner class ClickHandler(
83+
element: PsiElement,
84+
private val hint: String,
85+
) : (MouseEvent?, Editor) -> Unit {
86+
private val elementPointer = SmartPointerManager.createPointer(element)
87+
88+
override fun invoke(event: MouseEvent?, editor: Editor) {
89+
if (isInlaySettingsEditor(editor)) {
90+
return
91+
}
92+
val element = elementPointer.element ?: return
93+
logClickToFUS(element, hint)
94+
handleClick(editor, element, event)
95+
}
96+
}
97+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.mixin.insight.target
22+
23+
import com.demonwav.mcdev.asset.MCDevBundle
24+
import com.demonwav.mcdev.platform.mixin.action.FindMixinsAction
25+
import com.demonwav.mcdev.platform.mixin.util.isAccessorMixin
26+
import com.demonwav.mcdev.util.findReferencedClass
27+
import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering
28+
import com.intellij.openapi.editor.Editor
29+
import com.intellij.psi.PsiClass
30+
import com.intellij.psi.PsiElement
31+
import com.intellij.psi.PsiFile
32+
import java.awt.event.MouseEvent
33+
34+
class AccessorTargetCodeVisionProvider : AbstractMixinTargetCodeVisionProvider() {
35+
override val id = "mcdev.mixin.target.accessor"
36+
override val name: String
37+
get() = MCDevBundle("mixin.codeVision.target.accessor.name")
38+
override val relativeOrderings: List<CodeVisionRelativeOrdering>
39+
get() = super.relativeOrderings +
40+
CodeVisionRelativeOrdering.CodeVisionRelativeOrderingAfter(MixinTargetCodeVisionProvider.ID)
41+
42+
override fun acceptsElement(element: PsiElement) = element is PsiClass
43+
44+
override fun getHint(element: PsiElement, file: PsiFile): String? {
45+
val targetClass = element as? PsiClass ?: return null
46+
val numberOfMixins = FindMixinsAction.findMixins(targetClass, element.project)?.count { it.isAccessorMixin }
47+
?: return null
48+
if (numberOfMixins == 0) {
49+
return null
50+
}
51+
return MCDevBundle("mixin.codeVision.target.accessor.hint", numberOfMixins)
52+
}
53+
54+
override fun handleClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
55+
val project = editor.project ?: return
56+
val file = element.containingFile ?: return
57+
val targetClass = element.findReferencedClass() ?: return
58+
FindMixinsAction.openFindMixinsUI(project, editor, file, targetClass) { it.isAccessorMixin }
59+
}
60+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2025 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.mixin.insight.target
22+
23+
import com.demonwav.mcdev.asset.MCDevBundle
24+
import com.demonwav.mcdev.platform.mixin.action.FindMixinsAction
25+
import com.demonwav.mcdev.platform.mixin.util.isAccessorMixin
26+
import com.demonwav.mcdev.util.findReferencedClass
27+
import com.intellij.openapi.editor.Editor
28+
import com.intellij.psi.PsiClass
29+
import com.intellij.psi.PsiElement
30+
import com.intellij.psi.PsiFile
31+
import java.awt.event.MouseEvent
32+
33+
class MixinTargetCodeVisionProvider : AbstractMixinTargetCodeVisionProvider() {
34+
companion object {
35+
const val ID = "mcdev.mixin.target"
36+
}
37+
38+
override val id = ID
39+
override val name: String
40+
get() = MCDevBundle("mixin.codeVision.target.name")
41+
42+
override fun acceptsElement(element: PsiElement) = element is PsiClass
43+
44+
override fun getHint(element: PsiElement, file: PsiFile): String? {
45+
val targetClass = element as? PsiClass ?: return null
46+
val numberOfMixins = FindMixinsAction.findMixins(targetClass, element.project)?.count { !it.isAccessorMixin }
47+
?: return null
48+
if (numberOfMixins == 0) {
49+
return null
50+
}
51+
return MCDevBundle("mixin.codeVision.target.hint", numberOfMixins)
52+
}
53+
54+
override fun handleClick(editor: Editor, element: PsiElement, event: MouseEvent?) {
55+
val project = editor.project ?: return
56+
val file = element.containingFile ?: return
57+
val targetClass = element.findReferencedClass() ?: return
58+
FindMixinsAction.openFindMixinsUI(project, editor, file, targetClass) { !it.isAccessorMixin }
59+
}
60+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,9 @@
742742
<codeInsight.lineMarkerProvider language="JAVA"
743743
implementationClass="com.demonwav.mcdev.platform.mixin.insight.MixinElementLineMarkerProvider"/>
744744

745+
<codeInsight.daemonBoundCodeVisionProvider implementation="com.demonwav.mcdev.platform.mixin.insight.target.MixinTargetCodeVisionProvider"/>
746+
<codeInsight.daemonBoundCodeVisionProvider implementation="com.demonwav.mcdev.platform.mixin.insight.target.AccessorTargetCodeVisionProvider"/>
747+
745748

746749
<customJavadocTagProvider implementation="com.demonwav.mcdev.platform.mixin.MixinCustomJavaDocTagProvider"/>
747750

src/main/resources/messages/MinecraftDevelopment.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,11 @@ minecraft.settings.translation.use_custom_convert_template=Use custom template f
318318
minecraft.before=Before
319319
minecraft.after=After
320320

321+
mixin.codeVision.target.accessor.hint={0,choice, 0#no accessors|1#1 accessor|2#{0,number} accessors}
322+
mixin.codeVision.target.accessor.name=Accessor Mixins
323+
mixin.codeVision.target.hint={0,choice, 0#no mixins|1#1 mixin|2#{0,number} mixins}
324+
mixin.codeVision.target.name=Mixins
325+
321326
mixinextras.expression.lang.errors.array_access_missing_index=Missing index
322327
mixinextras.expression.lang.errors.array_length_after_empty=Cannot specify array length after an unspecified array length
323328
mixinextras.expression.lang.errors.empty_array_initializer=Array initializer cannot be empty

0 commit comments

Comments
 (0)