Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,40 @@

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
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() {

Expand All @@ -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)

Expand Down Expand Up @@ -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,
),
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,11 @@ class MixinReferenceContributor : PsiReferenceContributor() {
MethodDefinitionReference.ELEMENT_PATTERN,
MethodDefinitionReference,
)

// Mixin targets references
registrar.registerReferenceProvider(
MixinTargetsReference.ELEMENT_PATTERN,
MixinTargetsReference,
)
}
}
54 changes: 54 additions & 0 deletions src/main/kotlin/platform/mixin/reference/MixinTargetsReference.kt
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<PsiLiteral> = PsiJavaPatterns.psiLiteral(StandardPatterns.string())
.insideAnnotationAttribute(MIXIN, "targets")

override fun resolveReference(context: PsiElement): Array<ResolveResult> {
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<Any> = ArrayUtilRt.EMPTY_OBJECT_ARRAY
}
Loading