-
Notifications
You must be signed in to change notification settings - Fork 228
Support mixin targets reference and completion #2633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() { | ||
|
|
||
|
|
@@ -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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is a good idea. I'm concerned this could be too slow on slower machines. I think the easiest option would be to use After testing the PR I do think this is way too slow to be useable. |
||
| 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 |
|---|---|---|
| @@ -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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm missing something this call (and the whole delegating method) can be use the
MixinTargetsReference.ELEMENT_PATTERN. You just need to get thePsiLiteralthat position points to:val literal = PsiTreeUtil.getParentOfType(position, PsiLiteral::class.java).Then just
MixinTargetsReference.ELEMENT_PATTERN.accepts(literal).