Skip to content

Commit 47053d6

Browse files
committed
Add completion for At.args
1 parent 612b136 commit 47053d6

File tree

11 files changed

+417
-79
lines changed

11 files changed

+417
-79
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2024 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.completion
22+
23+
import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint
24+
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
25+
import com.demonwav.mcdev.util.constantStringValue
26+
import com.demonwav.mcdev.util.insideAnnotationAttribute
27+
import com.intellij.codeInsight.completion.CompletionContributor
28+
import com.intellij.codeInsight.completion.CompletionParameters
29+
import com.intellij.codeInsight.completion.CompletionProvider
30+
import com.intellij.codeInsight.completion.CompletionResultSet
31+
import com.intellij.codeInsight.completion.CompletionType
32+
import com.intellij.codeInsight.lookup.LookupElement
33+
import com.intellij.codeInsight.lookup.LookupElementBuilder
34+
import com.intellij.openapi.util.TextRange
35+
import com.intellij.patterns.PsiJavaPatterns
36+
import com.intellij.patterns.StandardPatterns
37+
import com.intellij.psi.JavaTokenType
38+
import com.intellij.psi.PsiAnnotation
39+
import com.intellij.psi.PsiLanguageInjectionHost
40+
import com.intellij.psi.PsiLiteral
41+
import com.intellij.psi.PsiNamedElement
42+
import com.intellij.psi.util.parentOfType
43+
import com.intellij.util.ProcessingContext
44+
45+
class AtArgsCompletionContributor : CompletionContributor() {
46+
init {
47+
for (tokenType in arrayOf(JavaTokenType.STRING_LITERAL, JavaTokenType.TEXT_BLOCK_LITERAL)) {
48+
extend(
49+
CompletionType.BASIC,
50+
PsiJavaPatterns.psiElement(tokenType).withParent(
51+
PsiJavaPatterns.psiLiteral(StandardPatterns.string())
52+
.insideAnnotationAttribute(MixinConstants.Annotations.AT, "args")
53+
),
54+
Provider,
55+
)
56+
}
57+
}
58+
59+
object Provider : CompletionProvider<CompletionParameters>() {
60+
override fun addCompletions(
61+
parameters: CompletionParameters,
62+
context: ProcessingContext,
63+
result: CompletionResultSet
64+
) {
65+
val literal = parameters.position.parentOfType<PsiLiteral>(withSelf = true) ?: return
66+
val atAnnotation = literal.parentOfType<PsiAnnotation>() ?: return
67+
val atCode = atAnnotation.findDeclaredAttributeValue("value")?.constantStringValue ?: return
68+
val injectionPoint = InjectionPoint.byAtCode(atCode) ?: return
69+
val escaper = (literal as? PsiLanguageInjectionHost)?.createLiteralTextEscaper() ?: return
70+
val beforeCursor = buildString {
71+
escaper.decode(TextRange(1, parameters.offset - (literal as PsiLiteral).textRange.startOffset), this)
72+
}
73+
val equalsIndex = beforeCursor.indexOf('=')
74+
if (equalsIndex == -1) {
75+
val argsKeys = injectionPoint.getArgsKeys(atAnnotation)
76+
result.addAllElements(
77+
argsKeys.map { completion ->
78+
LookupElementBuilder.create(if (completion.contains('=')) completion else "$completion=")
79+
.withPresentableText(completion)
80+
}
81+
)
82+
if (argsKeys.isNotEmpty()) {
83+
result.stopHere()
84+
}
85+
} else {
86+
val key = beforeCursor.substring(0, equalsIndex)
87+
val argsValues = injectionPoint.getArgsValues(atAnnotation, key)
88+
var prefix = beforeCursor.substring(equalsIndex + 1)
89+
if (injectionPoint.isArgValueList(atAnnotation, key)) {
90+
prefix = prefix.substringAfterLast(',').trimStart()
91+
}
92+
result.withPrefixMatcher(prefix).addAllElements(
93+
argsValues.map { completion ->
94+
when (completion) {
95+
is LookupElement -> completion
96+
is PsiNamedElement -> LookupElementBuilder.create(completion)
97+
else -> LookupElementBuilder.create(completion)
98+
}
99+
}
100+
)
101+
if (argsValues.isNotEmpty()) {
102+
result.stopHere()
103+
}
104+
}
105+
}
106+
}
107+
}

src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ import com.intellij.psi.PsiClass
3737
import com.intellij.psi.PsiClassType
3838
import com.intellij.psi.PsiElement
3939
import com.intellij.psi.PsiExpression
40+
import com.intellij.psi.PsiModifierList
4041
import com.intellij.psi.PsiQualifiedReference
4142
import com.intellij.psi.PsiReference
4243
import com.intellij.psi.PsiReferenceExpression
4344
import com.intellij.psi.search.GlobalSearchScope
4445
import com.intellij.psi.util.parentOfType
46+
import com.intellij.psi.util.parents
4547
import org.objectweb.asm.tree.ClassNode
4648
import org.objectweb.asm.tree.MethodNode
4749

@@ -132,6 +134,13 @@ class AtResolver(
132134
else -> constant.toString()
133135
}
134136
}
137+
138+
fun findInjectorAnnotation(at: PsiAnnotation): PsiAnnotation? {
139+
return at.parents(false)
140+
.takeWhile { it !is PsiClass }
141+
.filterIsInstance<PsiAnnotation>()
142+
.firstOrNull { it.parent is PsiModifierList }
143+
}
135144
}
136145

137146
fun isUnresolved(): InsnResolutionInfo.Failure? {

src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantInjectionPoint.kt

Lines changed: 137 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@
2020

2121
package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint
2222

23+
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
2324
import com.demonwav.mcdev.platform.mixin.reference.MixinSelector
25+
import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
2426
import com.demonwav.mcdev.util.constantValue
2527
import com.demonwav.mcdev.util.createLiteralExpression
2628
import com.demonwav.mcdev.util.descriptor
29+
import com.demonwav.mcdev.util.enumValueOfOrNull
2730
import com.demonwav.mcdev.util.ifNotBlank
31+
import com.demonwav.mcdev.util.mapToArray
32+
import com.demonwav.mcdev.util.toTypedArray
2833
import com.intellij.codeInsight.lookup.LookupElementBuilder
2934
import com.intellij.openapi.editor.Editor
3035
import com.intellij.openapi.project.Project
36+
import com.intellij.openapi.util.text.StringUtil
3137
import com.intellij.psi.CommonClassNames
3238
import com.intellij.psi.JavaPsiFacade
3339
import com.intellij.psi.JavaTokenType
@@ -43,9 +49,11 @@ import com.intellij.psi.PsiLiteral
4349
import com.intellij.psi.PsiLiteralExpression
4450
import com.intellij.psi.PsiSwitchLabelStatementBase
4551
import com.intellij.psi.util.PsiUtil
52+
import com.intellij.util.ArrayUtilRt
4653
import java.util.Locale
4754
import org.objectweb.asm.Opcodes
4855
import org.objectweb.asm.Type
56+
import org.objectweb.asm.tree.AbstractInsnNode
4957
import org.objectweb.asm.tree.ClassNode
5058
import org.objectweb.asm.tree.FrameNode
5159
import org.objectweb.asm.tree.InsnNode
@@ -57,10 +65,71 @@ import org.objectweb.asm.tree.MethodNode
5765
import org.objectweb.asm.tree.TypeInsnNode
5866

5967
class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
68+
companion object {
69+
private val ARGS_KEYS = arrayOf(
70+
"nullValue=true",
71+
"intValue",
72+
"floatValue",
73+
"longValue",
74+
"doubleValue",
75+
"stringValue",
76+
"classValue",
77+
"expandZeroConditions"
78+
)
79+
}
80+
6081
override fun onCompleted(editor: Editor, reference: PsiLiteral) {
6182
completeExtraStringAtAttribute(editor, reference, "args")
6283
}
6384

85+
override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS
86+
87+
override fun getArgsValues(at: PsiAnnotation, key: String): Array<Any> {
88+
fun collectTargets(constantToCompletion: (Any) -> Any?): Array<Any> {
89+
val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY
90+
val handler = injectorAnnotation.qualifiedName
91+
?.let { MixinAnnotationHandler.forMixinAnnotation(it, at.project) }
92+
?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY
93+
94+
val expandConditions = parseExpandConditions(AtResolver.getArgs(at))
95+
96+
return handler.resolveTarget(injectorAnnotation)
97+
.asSequence()
98+
.filterIsInstance<MethodTargetMember>()
99+
.flatMap { target ->
100+
target.classAndMethod.method.instructions?.let { insns ->
101+
Iterable { insns.iterator() }.asSequence()
102+
.mapNotNull { it.computeConstantValue(expandConditions) }
103+
.mapNotNull(constantToCompletion)
104+
} ?: emptySequence()
105+
}
106+
.toTypedArray()
107+
}
108+
109+
return when (key) {
110+
"expandZeroConditions" -> ExpandCondition.values().mapToArray { it.name.lowercase(Locale.ROOT) }
111+
"intValue" -> collectTargets { cst -> cst.takeIf { it is Int } }
112+
"floatValue" -> collectTargets { cst -> cst.takeIf { it is Float } }
113+
"longValue" -> collectTargets { cst -> cst.takeIf { it is Long } }
114+
"doubleValue" -> collectTargets { cst -> cst.takeIf { it is Double } }
115+
"stringValue" -> collectTargets { cst ->
116+
(cst as? String)?.let { str ->
117+
val escapedStr = StringUtil.escapeStringCharacters(str)
118+
when {
119+
str.isEmpty() -> null
120+
escapedStr.trim() != escapedStr -> LookupElementBuilder.create(escapedStr)
121+
.withPresentableText("\"${escapedStr}\"")
122+
else -> escapedStr
123+
}
124+
}
125+
}
126+
"classValue" -> collectTargets { cst -> (cst as? Type)?.internalName }
127+
else -> ArrayUtilRt.EMPTY_OBJECT_ARRAY
128+
}
129+
}
130+
131+
override fun isArgValueList(at: PsiAnnotation, key: String) = key == "expandZeroConditions"
132+
64133
fun getConstantInfo(at: PsiAnnotation): ConstantInfo? {
65134
val args = AtResolver.getArgs(at)
66135
val nullValue = args["nullValue"]?.let(java.lang.Boolean::parseBoolean) ?: false
@@ -88,19 +157,15 @@ class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
88157
intValue ?: floatValue ?: longValue ?: doubleValue ?: stringValue ?: classValue!!
89158
}
90159

91-
val expandConditions = args["expandZeroConditions"]
160+
return ConstantInfo(constant, parseExpandConditions(args))
161+
}
162+
163+
private fun parseExpandConditions(args: Map<String, String>): Set<ExpandCondition> {
164+
return args["expandZeroConditions"]
92165
?.replace(" ", "")
93166
?.split(',')
94-
?.mapNotNull {
95-
try {
96-
ExpandCondition.valueOf(it.uppercase(Locale.ENGLISH))
97-
} catch (e: IllegalArgumentException) {
98-
null
99-
}
100-
}
167+
?.mapNotNull { enumValueOfOrNull<ExpandCondition>(it.uppercase(Locale.ROOT)) }
101168
?.toSet() ?: emptySet()
102-
103-
return ConstantInfo(constant, expandConditions)
104169
}
105170

106171
private fun Boolean.toInt(): Int {
@@ -248,59 +313,10 @@ class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
248313
) : CollectVisitor<PsiElement>(mode) {
249314
override fun accept(methodNode: MethodNode) {
250315
methodNode.instructions?.iterator()?.forEachRemaining { insn ->
251-
val constant = when (insn) {
252-
is InsnNode -> when (insn.opcode) {
253-
in Opcodes.ICONST_M1..Opcodes.ICONST_5 -> insn.opcode - Opcodes.ICONST_0
254-
Opcodes.LCONST_0 -> 0L
255-
Opcodes.LCONST_1 -> 1L
256-
Opcodes.FCONST_0 -> 0.0f
257-
Opcodes.FCONST_1 -> 1.0f
258-
Opcodes.FCONST_2 -> 2.0f
259-
Opcodes.DCONST_0 -> 0.0
260-
Opcodes.DCONST_1 -> 1.0
261-
Opcodes.ACONST_NULL -> null
262-
else -> return@forEachRemaining
263-
}
264-
265-
is IntInsnNode -> when (insn.opcode) {
266-
Opcodes.BIPUSH, Opcodes.SIPUSH -> insn.operand
267-
else -> return@forEachRemaining
268-
}
269-
270-
is LdcInsnNode -> insn.cst
271-
is JumpInsnNode -> {
272-
if (constantInfo == null || !constantInfo.expandConditions.any { insn.opcode in it.opcodes }) {
273-
return@forEachRemaining
274-
}
275-
var lastInsn = insn.previous
276-
while (lastInsn != null && (lastInsn is LabelNode || lastInsn is FrameNode)) {
277-
lastInsn = lastInsn.previous
278-
}
279-
if (lastInsn != null) {
280-
val lastOpcode = lastInsn.opcode
281-
if (lastOpcode == Opcodes.LCMP ||
282-
lastOpcode == Opcodes.FCMPL ||
283-
lastOpcode == Opcodes.FCMPG ||
284-
lastOpcode == Opcodes.DCMPL ||
285-
lastOpcode == Opcodes.DCMPG
286-
) {
287-
return@forEachRemaining
288-
}
289-
}
290-
0
291-
}
292-
293-
is TypeInsnNode -> {
294-
if (insn.opcode < Opcodes.CHECKCAST) {
295-
// Don't treat NEW and ANEWARRAY as constants
296-
// Matches Mixin's handling
297-
return@forEachRemaining
298-
}
299-
Type.getObjectType(insn.desc)
300-
}
301-
302-
else -> return@forEachRemaining
303-
}
316+
val constant = (
317+
insn.computeConstantValue(constantInfo?.expandConditions ?: emptySet())
318+
?: return@forEachRemaining
319+
).let { if (it is NullSentinel) null else it }
304320

305321
if (constantInfo != null && constant != constantInfo.constant) {
306322
return@forEachRemaining
@@ -344,3 +360,61 @@ class ConstantInjectionPoint : InjectionPoint<PsiElement>() {
344360
}
345361
}
346362
}
363+
364+
private object NullSentinel
365+
366+
private fun AbstractInsnNode.computeConstantValue(expandConditions: Set<ConstantInjectionPoint.ExpandCondition>): Any? {
367+
return when (this) {
368+
is InsnNode -> when (opcode) {
369+
in Opcodes.ICONST_M1..Opcodes.ICONST_5 -> opcode - Opcodes.ICONST_0
370+
Opcodes.LCONST_0 -> 0L
371+
Opcodes.LCONST_1 -> 1L
372+
Opcodes.FCONST_0 -> 0.0f
373+
Opcodes.FCONST_1 -> 1.0f
374+
Opcodes.FCONST_2 -> 2.0f
375+
Opcodes.DCONST_0 -> 0.0
376+
Opcodes.DCONST_1 -> 1.0
377+
Opcodes.ACONST_NULL -> NullSentinel
378+
else -> null
379+
}
380+
381+
is IntInsnNode -> when (opcode) {
382+
Opcodes.BIPUSH, Opcodes.SIPUSH -> operand
383+
else -> null
384+
}
385+
386+
is LdcInsnNode -> cst
387+
is JumpInsnNode -> {
388+
if (expandConditions.none { opcode in it.opcodes }) {
389+
return null
390+
}
391+
var lastInsn = previous
392+
while (lastInsn != null && (lastInsn is LabelNode || lastInsn is FrameNode)) {
393+
lastInsn = lastInsn.previous
394+
}
395+
if (lastInsn != null) {
396+
val lastOpcode = lastInsn.opcode
397+
if (lastOpcode == Opcodes.LCMP ||
398+
lastOpcode == Opcodes.FCMPL ||
399+
lastOpcode == Opcodes.FCMPG ||
400+
lastOpcode == Opcodes.DCMPL ||
401+
lastOpcode == Opcodes.DCMPG
402+
) {
403+
return null
404+
}
405+
}
406+
0
407+
}
408+
409+
is TypeInsnNode -> {
410+
if (opcode < Opcodes.CHECKCAST) {
411+
// Don't treat NEW and ANEWARRAY as constants
412+
// Matches Mixin's handling
413+
return null
414+
}
415+
Type.getObjectType(desc)
416+
}
417+
418+
else -> null
419+
}
420+
}

0 commit comments

Comments
 (0)