From b0f2a7f32e090802a5b84a685de6e9ad6f1848f4 Mon Sep 17 00:00:00 2001 From: Enaium Date: Sat, 25 Jul 2026 18:16:55 +0800 Subject: [PATCH] Support mixin targets reference and completion --- .../completion/MixinCompletionContributor.kt | 102 ++++++++++++++++++ .../reference/MixinReferenceContributor.kt | 6 ++ .../mixin/reference/MixinTargetsReference.kt | 54 ++++++++++ 3 files changed, 162 insertions(+) create mode 100644 src/main/kotlin/platform/mixin/reference/MixinTargetsReference.kt diff --git a/src/main/kotlin/platform/mixin/completion/MixinCompletionContributor.kt b/src/main/kotlin/platform/mixin/completion/MixinCompletionContributor.kt index f6e7c3522..ed5801390 100644 --- a/src/main/kotlin/platform/mixin/completion/MixinCompletionContributor.kt +++ b/src/main/kotlin/platform/mixin/completion/MixinCompletionContributor.kt @@ -20,8 +20,10 @@ package com.demonwav.mcdev.platform.mixin.completion +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MIXIN import com.demonwav.mcdev.platform.mixin.util.findShadowTargets import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.equivalentTo import com.demonwav.mcdev.util.filter import com.demonwav.mcdev.util.findContainingClass @@ -29,17 +31,29 @@ import com.intellij.codeInsight.completion.CompletionContributor import com.intellij.codeInsight.completion.CompletionParameters import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.completion.CompletionUtil import com.intellij.codeInsight.completion.JavaCompletionContributor import com.intellij.codeInsight.completion.JavaCompletionSorting import com.intellij.codeInsight.completion.LegacyCompletionContributor +import com.intellij.codeInsight.completion.PrioritizedLookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement import com.intellij.psi.PsiExpression import com.intellij.psi.PsiJavaReference +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiNameValuePair import com.intellij.psi.PsiQualifiedReference import com.intellij.psi.PsiSuperExpression import com.intellij.psi.PsiThisExpression +import com.intellij.psi.search.PsiShortNamesCache +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.PlatformIcons class MixinCompletionContributor : CompletionContributor() { @@ -59,6 +73,12 @@ class MixinCompletionContributor : CompletionContributor() { return } + // Check if completing inside @Mixin targets attribute + if (isInsideMixinTargets(position)) { + provideMixinTargetsCompletion(position, result) + return + } + // Run all the other contributors first result.runRemainingContributors(parameters, result::passResult) @@ -123,4 +143,86 @@ class MixinCompletionContributor : CompletionContributor() { r.addAllElements(elements) } } + + private fun isInsideMixinTargets(position: PsiElement): Boolean { + val literal = PsiTreeUtil.getParentOfType(position, PsiLiteral::class.java) ?: return false + if (literal.value !is String) return false + var current = literal.parent + while (current != null) { + when (current) { + is PsiNameValuePair -> { + return current.attributeName == "targets" && + current.parent?.parent is PsiAnnotation && + (current.parent.parent as PsiAnnotation).qualifiedName == MIXIN + } + + is PsiAnnotation -> return false + is PsiClass -> return false + } + current = current.parent + } + return false + } + + private fun provideMixinTargetsCompletion(position: PsiElement, result: CompletionResultSet) { + val literal = PsiTreeUtil.getParentOfType(position, PsiLiteral::class.java) ?: return + val text = literal.constantStringValue?.removeSuffix(CompletionUtil.DUMMY_IDENTIFIER) ?: "" + + val project = position.project + val scope = position.resolveScope + val facade = JavaPsiFacade.getInstance(project) + + val parts = text.split('.').dropLast(1) + val packageName = parts.joinToString(".") + + // Show packages and classes from the parent package + val pkg = facade.findPackage(packageName.ifEmpty { "" }) + if (pkg != null) { + for (subPkg in pkg.getSubPackages(scope)) { + val subPkgName = subPkg.qualifiedName + val subPkgSimpleName = subPkg.name ?: continue + result.addElement( + PrioritizedLookupElement.withPriority( + LookupElementBuilder.create(subPkgName) + .withPresentableText(subPkgSimpleName) + .withIcon(PlatformIcons.PACKAGE_ICON), + 1.0, + ), + ) + } + for (cls in pkg.getClasses(scope)) { + val fqn = cls.qualifiedName ?: continue + val simpleName = fqn.substringAfterLast('.') + result.addElement( + PrioritizedLookupElement.withPriority( + LookupElementBuilder.create(fqn) + .withPresentableText(simpleName) + .withLookupString(simpleName) + .withIcon(cls.getIcon(0)), + 0.5, + ), + ) + } + } + + // When input is empty, also show all class names (limited) + val cache = PsiShortNamesCache.getInstance(project) + if (packageName.isEmpty() && text.firstOrNull()?.isUpperCase() == true) { + for (className in cache.allClassNames) { + for (cls in cache.getClassesByName(className, scope)) { + val fqn = cls.qualifiedName ?: continue + val simpleName = fqn.substringAfterLast('.') + result.addElement( + PrioritizedLookupElement.withPriority( + LookupElementBuilder.create(fqn) + .withPresentableText(simpleName) + .withLookupString(simpleName) + .withIcon(cls.getIcon(0)), + 0.5, + ), + ) + } + } + } + } } diff --git a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt index 5bef6a319..1dea1f4af 100644 --- a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt +++ b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt @@ -72,5 +72,11 @@ class MixinReferenceContributor : PsiReferenceContributor() { MethodDefinitionReference.ELEMENT_PATTERN, MethodDefinitionReference, ) + + // Mixin targets references + registrar.registerReferenceProvider( + MixinTargetsReference.ELEMENT_PATTERN, + MixinTargetsReference, + ) } } diff --git a/src/main/kotlin/platform/mixin/reference/MixinTargetsReference.kt b/src/main/kotlin/platform/mixin/reference/MixinTargetsReference.kt new file mode 100644 index 000000000..e4bf7dbea --- /dev/null +++ b/src/main/kotlin/platform/mixin/reference/MixinTargetsReference.kt @@ -0,0 +1,54 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.reference + +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MIXIN +import com.demonwav.mcdev.util.findQualifiedClass +import com.demonwav.mcdev.util.insideAnnotationAttribute +import com.demonwav.mcdev.util.reference.PolyReferenceResolver +import com.intellij.patterns.ElementPattern +import com.intellij.patterns.PsiJavaPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementResolveResult +import com.intellij.psi.PsiLiteral +import com.intellij.psi.ResolveResult +import com.intellij.util.ArrayUtilRt + +/** + * Provides reference resolution for the `targets` attribute of the `@Mixin` annotation. + * + * Resolves fully qualified class name strings to their corresponding PsiClass for navigation (Ctrl+Click). + */ +object MixinTargetsReference : PolyReferenceResolver() { + + val ELEMENT_PATTERN: ElementPattern = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) + .insideAnnotationAttribute(MIXIN, "targets") + + override fun resolveReference(context: PsiElement): Array { + val fqn = (context as? PsiLiteral)?.value as? String ?: return ResolveResult.EMPTY_ARRAY + val psiClass = findQualifiedClass(context.project, fqn, context.resolveScope) + ?: return ResolveResult.EMPTY_ARRAY + return arrayOf(PsiElementResolveResult(psiClass)) + } + + override fun collectVariants(context: PsiElement): Array = ArrayUtilRt.EMPTY_OBJECT_ARRAY +} \ No newline at end of file