Skip to content

Commit 6ec969c

Browse files
authored
Fold invoker and accessor Mixin method calls to appear as the target method name (#847)
This folder attempts to make reading code which use invokers and accessors a little cleaner by showing the user the method they are actually calling in the end, rather than the large cast and weird method name of the invoker. The folds are configurable into 4 separate sections: 1. Invoker method call fold 2. Invoker cast expression fold 3. Accessor method call fold 4. Accessor cast expression fold They are all enabled by default except for the accessor method call fold, since some people may think the setter fold looks a little clunky. This commit also improves the code folding options page by setting a title for the fold groups, which presents them in a category together and generally looks better.
1 parent 6ce2538 commit 6ec969c

9 files changed

+374
-71
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Minecraft Dev for IntelliJ
3+
*
4+
* https://minecraftdev.org
5+
*
6+
* Copyright (c) 2021 minecraft-dev
7+
*
8+
* MIT License
9+
*/
10+
11+
package com.demonwav.mcdev.platform.mixin.folding
12+
13+
import com.demonwav.mcdev.platform.mixin.MixinModuleType
14+
import com.demonwav.mcdev.platform.mixin.util.findAccessorAnnotation
15+
import com.demonwav.mcdev.platform.mixin.util.findAccessorTarget
16+
import com.demonwav.mcdev.platform.mixin.util.findInvokerAnnotation
17+
import com.demonwav.mcdev.platform.mixin.util.findInvokerTarget
18+
import com.demonwav.mcdev.util.referencedMethod
19+
import com.intellij.lang.ASTNode
20+
import com.intellij.lang.folding.CustomFoldingBuilder
21+
import com.intellij.lang.folding.FoldingDescriptor
22+
import com.intellij.openapi.editor.Document
23+
import com.intellij.openapi.editor.FoldingGroup
24+
import com.intellij.openapi.util.TextRange
25+
import com.intellij.psi.JavaRecursiveElementWalkingVisitor
26+
import com.intellij.psi.JavaTokenType
27+
import com.intellij.psi.PsiElement
28+
import com.intellij.psi.PsiIdentifier
29+
import com.intellij.psi.PsiJavaFile
30+
import com.intellij.psi.PsiMethodCallExpression
31+
import com.intellij.psi.PsiParenthesizedExpression
32+
import com.intellij.psi.PsiType
33+
import com.intellij.psi.PsiTypeCastExpression
34+
import com.intellij.psi.util.elementType
35+
import com.intellij.psi.util.parentOfType
36+
37+
class AccessorMixinFoldingBuilder : CustomFoldingBuilder() {
38+
39+
override fun isDumbAware(): Boolean = false
40+
41+
override fun isRegionCollapsedByDefault(node: ASTNode): Boolean {
42+
val settings = MixinFoldingSettings.instance.state
43+
val element = node.psi
44+
val parent = element.parentOfType<PsiMethodCallExpression>() ?: return false
45+
val refMethod = parent.referencedMethod
46+
47+
return when {
48+
refMethod == null -> false
49+
refMethod.findInvokerAnnotation() != null -> when (element) {
50+
is PsiIdentifier -> settings.foldInvokerMethodCalls
51+
is PsiParenthesizedExpression -> settings.foldInvokerCasts
52+
else -> false
53+
}
54+
refMethod.findAccessorAnnotation() != null -> when (element) {
55+
is PsiIdentifier -> settings.foldAccessorMethodCalls
56+
is PsiParenthesizedExpression -> settings.foldAccessorCasts
57+
else -> false
58+
}
59+
else -> false
60+
}
61+
}
62+
63+
override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String {
64+
// Accessor parentheses
65+
if (node.elementType == JavaTokenType.LPARENTH || node.elementType == JavaTokenType.RPARENTH) {
66+
return ""
67+
}
68+
69+
return when (val element = node.psi) {
70+
is PsiParenthesizedExpression -> foldAccessorCastExpression(element)
71+
is PsiIdentifier -> foldAccessorIdentifier(element)
72+
else -> null
73+
} ?: node.text
74+
}
75+
76+
private fun foldAccessorIdentifier(identifier: PsiIdentifier): String? {
77+
val expr = identifier.parentOfType<PsiMethodCallExpression>() ?: return null
78+
val method = expr.referencedMethod ?: return null
79+
80+
if (method.findInvokerAnnotation() != null) {
81+
return method.findInvokerTarget()?.element?.name
82+
}
83+
if (method.findAccessorAnnotation() != null) {
84+
val name = method.findAccessorTarget()?.element?.name ?: return null
85+
return if (method.returnType == PsiType.VOID) {
86+
"$name = "
87+
} else {
88+
name
89+
}
90+
}
91+
92+
return null
93+
}
94+
95+
private fun foldAccessorCastExpression(expr: PsiParenthesizedExpression): String? {
96+
val castExpression = expr.expression as? PsiTypeCastExpression ?: return null
97+
return castExpression.operand?.text
98+
}
99+
100+
override fun buildLanguageFoldRegions(
101+
descriptors: MutableList<FoldingDescriptor>,
102+
root: PsiElement,
103+
document: Document,
104+
quick: Boolean
105+
) {
106+
if (root !is PsiJavaFile || !MixinModuleType.isInModule(root)) {
107+
return
108+
}
109+
110+
root.accept(Visitor(descriptors))
111+
}
112+
113+
private class Visitor(private val descriptors: MutableList<FoldingDescriptor>) :
114+
JavaRecursiveElementWalkingVisitor() {
115+
116+
val settings = MixinFoldingSettings.instance.state
117+
118+
override fun visitMethodCallExpression(expression: PsiMethodCallExpression) {
119+
super.visitMethodCallExpression(expression)
120+
121+
if (
122+
!settings.foldInvokerCasts && !settings.foldInvokerMethodCalls &&
123+
!settings.foldAccessorCasts && !settings.foldAccessorMethodCalls
124+
) {
125+
return
126+
}
127+
128+
// This is what we're actually going to fold, not the PsiMethodCallExpression
129+
val referenceExpression = expression.methodExpression
130+
131+
val refMethod = expression.referencedMethod ?: return
132+
133+
val invoker = refMethod.findInvokerAnnotation() != null
134+
if (invoker && !settings.foldInvokerCasts && !settings.foldInvokerMethodCalls) {
135+
return
136+
}
137+
val accessor = refMethod.findAccessorAnnotation() != null
138+
if (accessor && !settings.foldAccessorCasts && !settings.foldAccessorMethodCalls) {
139+
return
140+
}
141+
142+
// The final element must be a method name (identifier)
143+
val identifier = referenceExpression.lastChild as? PsiIdentifier ?: return
144+
if (invoker && settings.foldInvokerMethodCalls) {
145+
descriptors.add(
146+
FoldingDescriptor(
147+
identifier.node,
148+
identifier.textRange
149+
)
150+
)
151+
}
152+
153+
if (accessor && settings.foldAccessorMethodCalls) {
154+
foldAccessorMethodCall(expression, identifier)
155+
}
156+
157+
// We have folded the method call by this point
158+
// Now we need to decide to fold the cast expression
159+
// Note: There may not be a cast expression
160+
161+
// The pattern we're expecting is something like:
162+
// ((MixinWorldAccessor) world).callSomeMethod(...)
163+
// So if there's a cast expression here it'll be inside a parenthisized expression
164+
val parenthetical = referenceExpression.firstChild as? PsiParenthesizedExpression ?: return
165+
val castExpression = parenthetical.expression as? PsiTypeCastExpression ?: return
166+
167+
// Can't fold without an operand
168+
if (castExpression.operand == null) {
169+
return
170+
}
171+
172+
if ((invoker && settings.foldInvokerCasts) || (accessor && settings.foldAccessorCasts)) {
173+
descriptors.add(
174+
FoldingDescriptor(
175+
parenthetical.node,
176+
parenthetical.textRange
177+
)
178+
)
179+
}
180+
}
181+
182+
private fun foldAccessorMethodCall(
183+
expression: PsiMethodCallExpression,
184+
identifier: PsiIdentifier
185+
) {
186+
val argumentList = expression.argumentList
187+
val openParen = argumentList.firstChild
188+
val closeParen = argumentList.lastChild
189+
if (openParen.elementType !== JavaTokenType.LPARENTH || closeParen.elementType !== JavaTokenType.RPARENTH) {
190+
return
191+
}
192+
193+
// All of these folds go together
194+
val group = FoldingGroup.newGroup("accessMethodCall")
195+
196+
// For a getter we could do both () in a single fold
197+
// but for a setter we have to do them separate (since there's an expression in side of them)
198+
// In practice it doesn't really matter, so just always handling them separate reduce's the number of
199+
// special cases the code has to understand.
200+
descriptors.add(
201+
FoldingDescriptor(
202+
openParen.node,
203+
openParen.textRange,
204+
group
205+
)
206+
)
207+
descriptors.add(
208+
FoldingDescriptor(
209+
closeParen.node,
210+
closeParen.textRange,
211+
group
212+
)
213+
)
214+
215+
descriptors.add(
216+
FoldingDescriptor(
217+
identifier.node,
218+
identifier.textRange,
219+
group
220+
)
221+
)
222+
}
223+
}
224+
}

src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,34 @@ class MixinFoldingOptionsProvider :
1717
BeanConfigurable<MixinFoldingSettings.State>(MixinFoldingSettings.instance.state), CodeFoldingOptionsProvider {
1818

1919
init {
20+
title = "Mixin"
21+
2022
val settings = MixinFoldingSettings.instance
2123
checkBox(
22-
"Mixin: Target descriptors",
24+
"Target descriptors",
2325
{ settings.state.foldTargetDescriptors },
2426
{ b -> settings.state.foldTargetDescriptors = b }
2527
)
26-
checkBox("Mixin: Object casts", { settings.state.foldObjectCasts }, { b -> settings.state.foldObjectCasts = b })
28+
checkBox("Object casts", { settings.state.foldObjectCasts }, { b -> settings.state.foldObjectCasts = b })
29+
checkBox(
30+
"Invoker casts",
31+
{ settings.state.foldInvokerCasts },
32+
{ b -> settings.state.foldInvokerCasts = b }
33+
)
34+
checkBox(
35+
"Invoker method calls",
36+
{ settings.state.foldInvokerMethodCalls },
37+
{ b -> settings.state.foldInvokerMethodCalls = b }
38+
)
39+
checkBox(
40+
"Accessor casts",
41+
{ settings.state.foldAccessorCasts },
42+
{ b -> settings.state.foldAccessorCasts = b }
43+
)
44+
checkBox(
45+
"Accessor method calls",
46+
{ settings.state.foldAccessorMethodCalls },
47+
{ b -> settings.state.foldAccessorMethodCalls = b }
48+
)
2749
}
2850
}

src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ class MixinFoldingSettings : PersistentStateComponent<MixinFoldingSettings.State
2020

2121
data class State(
2222
var foldTargetDescriptors: Boolean = true,
23-
var foldObjectCasts: Boolean = false
23+
var foldObjectCasts: Boolean = false,
24+
var foldInvokerCasts: Boolean = true,
25+
var foldInvokerMethodCalls: Boolean = true,
26+
var foldAccessorCasts: Boolean = true,
27+
var foldAccessorMethodCalls: Boolean = false,
2428
)
2529

2630
private var state = State()

src/main/kotlin/platform/mixin/folding/MixinFoldingBuilder.kt renamed to src/main/kotlin/platform/mixin/folding/MixinObjectCastFoldingBuilder.kt

Lines changed: 6 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,72 +11,31 @@
1111
package com.demonwav.mcdev.platform.mixin.folding
1212

1313
import com.demonwav.mcdev.platform.mixin.MixinModuleType
14-
import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference
15-
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT
1614
import com.intellij.lang.ASTNode
1715
import com.intellij.lang.folding.CustomFoldingBuilder
1816
import com.intellij.lang.folding.FoldingDescriptor
19-
import com.intellij.navigation.NavigationItem
2017
import com.intellij.openapi.editor.Document
2118
import com.intellij.openapi.util.TextRange
2219
import com.intellij.psi.CommonClassNames
2320
import com.intellij.psi.JavaRecursiveElementWalkingVisitor
24-
import com.intellij.psi.PsiAnnotation
25-
import com.intellij.psi.PsiAnnotationMemberValue
2621
import com.intellij.psi.PsiClassType
2722
import com.intellij.psi.PsiElement
2823
import com.intellij.psi.PsiJavaFile
29-
import com.intellij.psi.PsiMethod
30-
import com.intellij.psi.PsiSubstitutor
3124
import com.intellij.psi.PsiTypeCastExpression
32-
import com.intellij.psi.PsiVariable
3325
import com.intellij.psi.impl.source.tree.ChildRole
3426
import com.intellij.psi.impl.source.tree.CompositeElement
35-
import com.intellij.psi.util.PsiFormatUtil
36-
import com.intellij.psi.util.PsiFormatUtilBase
37-
import com.intellij.psi.util.PsiFormatUtilBase.SHOW_CONTAINING_CLASS
3827

39-
class MixinFoldingBuilder : CustomFoldingBuilder() {
28+
class MixinObjectCastFoldingBuilder : CustomFoldingBuilder() {
4029

4130
// I'm not dumb
4231
override fun isDumbAware() = false
4332

44-
override fun isRegionCollapsedByDefault(node: ASTNode): Boolean {
45-
val settings = MixinFoldingSettings.instance.state
46-
return when (node.psi) {
47-
is PsiTypeCastExpression -> settings.foldObjectCasts
48-
is PsiAnnotationMemberValue -> settings.foldTargetDescriptors
49-
else -> true
50-
}
51-
}
52-
53-
private fun formatElement(element: PsiElement): String? {
54-
return when (element) {
55-
is PsiMethod -> PsiFormatUtil.formatMethod(
56-
element,
57-
PsiSubstitutor.EMPTY,
58-
PsiFormatUtilBase.SHOW_NAME or PsiFormatUtilBase.SHOW_PARAMETERS or SHOW_CONTAINING_CLASS,
59-
PsiFormatUtilBase.SHOW_TYPE
60-
)
61-
is PsiVariable -> PsiFormatUtil.formatVariable(
62-
element,
63-
PsiFormatUtilBase.SHOW_NAME or SHOW_CONTAINING_CLASS,
64-
PsiSubstitutor.EMPTY
65-
)
66-
is NavigationItem -> element.presentation?.presentableText
67-
else -> null
68-
}
69-
}
33+
override fun isRegionCollapsedByDefault(node: ASTNode): Boolean =
34+
MixinFoldingSettings.instance.state.foldObjectCasts
7035

71-
override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String {
72-
val element = node.psi
73-
return when (element) {
74-
is PsiTypeCastExpression -> "(${element.castType?.text ?: return node.text})"
75-
is PsiAnnotationMemberValue ->
76-
TargetReference.resolveTarget(element)?.let { formatElement(it) }
77-
?: node.text
78-
else -> node.text
79-
}
36+
override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String? {
37+
val element = node.psi as? PsiTypeCastExpression ?: return null
38+
return "(${element.castType?.text ?: return node.text})"
8039
}
8140

8241
override fun buildLanguageFoldRegions(
@@ -97,22 +56,6 @@ class MixinFoldingBuilder : CustomFoldingBuilder() {
9756

9857
val settings = MixinFoldingSettings.instance.state
9958

100-
override fun visitAnnotation(annotation: PsiAnnotation) {
101-
super.visitAnnotation(annotation)
102-
103-
if (!settings.foldTargetDescriptors) {
104-
return
105-
}
106-
107-
val qualifiedName = annotation.qualifiedName ?: return
108-
if (qualifiedName != AT) {
109-
return
110-
}
111-
112-
val target = annotation.findDeclaredAttributeValue("target") ?: return
113-
descriptors.add(FoldingDescriptor(target, target.textRange))
114-
}
115-
11659
override fun visitTypeCastExpression(expression: PsiTypeCastExpression) {
11760
super.visitTypeCastExpression(expression)
11861

0 commit comments

Comments
 (0)