User: joe Date: 08 Apr 26 01:30 Revision: 9ab87fb8faf78967c1ebb9900951156f4a06ea58 Summary: Add warning for mixin added enum constants not prefixed by the mod ID TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10494&personal=false Index: src/main/kotlin/platform/AbstractModule.kt =================================================================== --- src/main/kotlin/platform/AbstractModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/AbstractModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -24,12 +24,17 @@ import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.inspection.IsCancelled import com.intellij.openapi.module.Module +import com.intellij.openapi.util.Key import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiExpression import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.PsiModificationTracker import javax.swing.Icon abstract class AbstractModule(protected val facet: MinecraftFacet) { @@ -44,6 +49,21 @@ open val icon: Icon? get() = moduleType.icon + protected open fun computeModIds(): List = emptyList() + + private val modIdsCacheKey = Key.create>>("modIds for ${javaClass.simpleName}") + + /** + * Returns a list of mod IDs for this module. Must be called in a read action! + */ + val modIds: List + get() { + val provider = CachedValueProvider { + CachedValueProvider.Result(computeModIds(), PsiModificationTracker.MODIFICATION_COUNT) + } + return CachedValuesManager.getManager(project).getCachedValue(module, modIdsCacheKey, provider, false) + } + open val eventListenerGenSupport: EventListenerGenerationSupport? = null /** Index: src/main/kotlin/platform/bukkit/BukkitModule.kt =================================================================== --- src/main/kotlin/platform/bukkit/BukkitModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/bukkit/BukkitModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -29,23 +29,26 @@ import com.demonwav.mcdev.platform.bukkit.util.BukkitConstants import com.demonwav.mcdev.platform.bukkit.util.PaperConstants import com.demonwav.mcdev.util.SourceType -import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.nullable import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.util.application import org.jetbrains.uast.UClass import org.jetbrains.uast.UIdentifier import org.jetbrains.uast.toUElementOfType +import org.jetbrains.yaml.YAMLUtil +import org.jetbrains.yaml.psi.YAMLFile class BukkitModule>(facet: MinecraftFacet, type: T) : AbstractModule(facet) { @@ -66,6 +69,8 @@ override val type: PlatformType = type.platformType + override fun computeModIds() = getPluginNames(project, pluginYml) + override val moduleType: T = type override val eventListenerGenSupport: EventListenerGenerationSupport = BukkitEventListenerGenerationSupport() @@ -175,4 +180,12 @@ pluginYml = null } + + companion object { + fun getPluginNames(project: Project, pluginYml: VirtualFile?): List { + val yamlFile = PsiManager.getInstance(project).findFile(pluginYml ?: return emptyList()) as? YAMLFile + ?: return emptyList() + return listOfNotNull(YAMLUtil.getValue(yamlFile, "name")?.second) -} + } + } +} Index: src/main/kotlin/platform/bungeecord/BungeeCordModule.kt =================================================================== --- src/main/kotlin/platform/bungeecord/BungeeCordModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/bungeecord/BungeeCordModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -25,6 +25,7 @@ import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.AbstractModuleType import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.bukkit.BukkitModule import com.demonwav.mcdev.platform.bukkit.BukkitModuleType import com.demonwav.mcdev.platform.bukkit.PaperModuleType import com.demonwav.mcdev.platform.bukkit.SpigotModuleType @@ -65,6 +66,8 @@ override val moduleType: T = type + override fun computeModIds() = BukkitModule.getPluginNames(project, pluginYml) + override val eventListenerGenSupport: EventListenerGenerationSupport = BungeeCordEventListenerGenerationSupport() override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?) = Index: src/main/kotlin/platform/fabric/FabricModule.kt =================================================================== --- src/main/kotlin/platform/fabric/FabricModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/fabric/FabricModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -33,8 +33,12 @@ import com.demonwav.mcdev.util.SourceType import com.demonwav.mcdev.util.nullable import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions +import com.intellij.json.JsonUtil +import com.intellij.json.psi.JsonFile +import com.intellij.json.psi.JsonStringLiteral import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod import com.intellij.psi.search.searches.ReferencesSearch import java.io.IOException @@ -64,6 +68,13 @@ override val type = PlatformType.FABRIC override val icon = PlatformAssets.FABRIC_ICON + override fun computeModIds(): List { + val jsonFile = PsiManager.getInstance(project).findFile(fabricJson ?: return emptyList()) as? JsonFile + ?: return emptyList() + val jsonObj = JsonUtil.getTopLevelObject(jsonFile) ?: return emptyList() + return listOfNotNull((jsonObj.findProperty("id")?.value as? JsonStringLiteral)?.value) + } + override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?) = true override fun writeErrorMessageForEventParameter(eventClass: PsiClass, method: PsiMethod) = "" Index: src/main/kotlin/platform/forge/ForgeModule.kt =================================================================== --- src/main/kotlin/platform/forge/ForgeModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/forge/ForgeModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -29,6 +29,7 @@ import com.demonwav.mcdev.platform.forge.inspections.sideonly.SidedProxyAnnotator import com.demonwav.mcdev.platform.forge.util.ForgeConstants import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.toml.stringValue import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.SourceType import com.demonwav.mcdev.util.nullable @@ -40,9 +41,12 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ReadAction import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.search.GlobalSearchScope @@ -52,16 +56,23 @@ import org.jetbrains.uast.UClass import org.jetbrains.uast.UIdentifier import org.jetbrains.uast.toUElementOfType +import org.toml.lang.psi.TomlArrayTable +import org.toml.lang.psi.TomlFile +import org.toml.lang.psi.ext.name class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(facet) { var mcmod by nullable { facet.findFile(ForgeConstants.MCMOD_INFO, SourceType.RESOURCE) } private set + var modsToml by nullable { facet.findFile(ForgeConstants.MODS_TOML_PATH, SourceType.RESOURCE) } + private set override val moduleType = ForgeModuleType override val type = PlatformType.FORGE override val icon = PlatformAssets.FORGE_ICON + override fun computeModIds() = getModsFromModsToml(project, modsToml) + override val eventListenerGenSupport: EventListenerGenerationSupport = ForgeEventListenerGenerationSupport() override fun init() { @@ -168,6 +179,28 @@ override fun dispose() { mcmod = null + modsToml = null super.dispose() } + + companion object { + fun getModsFromModsToml(project: Project, modsToml: VirtualFile?): List { + val tomlFile = PsiManager.getInstance(project).findFile(modsToml ?: return emptyList()) as? TomlFile + ?: return emptyList() + + val result = mutableListOf() + + for (child in tomlFile.children) { + if (child is TomlArrayTable && child.header.key?.name == "mods") { + for (entry in child.entries) { + if (entry.key.name == "modId") { + entry.value?.stringValue()?.let { result += it } -} + } + } + } + } + + return result + } + } +} Index: src/main/kotlin/platform/forge/util/ForgeConstants.kt =================================================================== --- src/main/kotlin/platform/forge/util/ForgeConstants.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/forge/util/ForgeConstants.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -37,6 +37,7 @@ const val NETWORK_MESSAGE_HANDLER = "net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler" const val MCMOD_INFO = "mcmod.info" const val MODS_TOML = "mods.toml" + const val MODS_TOML_PATH = "META-INF/$MODS_TOML" const val PACK_MCMETA = "pack.mcmeta" const val JAR_VERSION_VAR = $$"${file.jarVersion}" Index: src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -22,11 +22,15 @@ import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.platform.fabric.FabricModuleType +import com.demonwav.mcdev.util.RegexConverter import com.demonwav.mcdev.util.decapitalize +import com.demonwav.mcdev.util.doBindItem +import com.demonwav.mcdev.util.doBindText import com.demonwav.mcdev.util.findContainingClass import com.demonwav.mcdev.util.findModule -import com.demonwav.mcdev.util.onShown +import com.demonwav.mcdev.util.regexValidator import com.demonwav.mcdev.util.toJavaIdentifier +import com.demonwav.mcdev.util.toRegexOrDefault import com.intellij.codeInsight.CodeInsightBundle import com.intellij.codeInsight.FileModificationService import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo @@ -37,11 +41,9 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.ComponentValidator -import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiElement +import com.intellij.psi.PsiEnumConstant import com.intellij.psi.PsiField import com.intellij.psi.PsiFile import com.intellij.psi.PsiMethod @@ -49,25 +51,14 @@ import com.intellij.psi.PsiNamedElement import com.intellij.psi.util.PsiTreeUtil import com.intellij.refactoring.rename.RenameProcessor -import com.intellij.ui.DocumentAdapter import com.intellij.ui.EnumComboBoxModel import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBTextField import com.intellij.ui.dsl.builder.COLUMNS_SHORT -import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.RowLayout import com.intellij.ui.dsl.builder.columns import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.layout.ValidationInfoBuilder -import com.intellij.util.xmlb.Converter import com.intellij.util.xmlb.annotations.Attribute -import java.util.function.Supplier -import java.util.regex.Pattern -import java.util.regex.PatternSyntaxException import javax.swing.JComponent -import javax.swing.event.DocumentEvent -import kotlin.reflect.KMutableProperty0 -import org.intellij.lang.annotations.Language class AddedMembersNameFormatInspection : AbstractAddedMembersInspection() { @Attribute(converter = RegexConverter::class) @@ -91,7 +82,7 @@ override fun getStaticDescription() = "Reports added members not matching the correct name format" override fun visitAddedField(holder: ProblemsHolder, field: PsiField) { - if (reportFields.shouldReport(field.findModule())) { + if (field !is PsiEnumConstant && reportFields.shouldReport(field.findModule())) { visitAdded(holder, field) } } @@ -131,7 +122,7 @@ val fixed = try { validNameFixSearch.replace(name, validNameFixReplace) .replace("MOD_ID", getAppropriatePrefix(holder.project)) - } catch (e: RuntimeException) { + } catch (_: RuntimeException) { null } @@ -219,67 +210,6 @@ } } -private fun String.toRegexOrDefault(@Language("RegExp") default: String): Regex { - return try { - this.toRegex() - } catch (e: PatternSyntaxException) { - default.toRegex() - } -} - -private fun Cell.doBindText(property: KMutableProperty0): Cell { - return doBindText(property.getter, property.setter) -} - -private fun Cell.doBindText(getter: () -> String, setter: (String) -> Unit): Cell { - component.text = getter() - component.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(e: DocumentEvent) { - setter(component.text) - } - }) - return this -} - -private fun Cell>.doBindItem(property: KMutableProperty0): Cell> { - component.selectedItem = property.get() - component.addActionListener { - @Suppress("UNCHECKED_CAST") - val selectedItem = component.selectedItem as T? - if (selectedItem != null) { - property.set(selectedItem) - } - } - return this -} - -private fun Cell.regexValidator(): Cell { - var hasRegisteredValidator = false - component.onShown { - if (!hasRegisteredValidator) { - hasRegisteredValidator = true - val disposable = DialogWrapper.findInstance(component)?.disposable ?: return@onShown - ComponentValidator(disposable).withValidator( - Supplier { - try { - Pattern.compile(component.text) - null - } catch (e: PatternSyntaxException) { - ValidationInfoBuilder(component).error("Invalid regex") - } - } - ).andRegisterOnDocumentListener(component).installOn(component) - } - } - return this -} - -private class RegexConverter : Converter() { - override fun toString(value: Regex) = value.pattern - - override fun fromString(value: String) = runCatching { value.toRegex() }.getOrNull() -} - private class RenameWithInheritanceFix( element: PsiNamedElement, private val newName: String Index: src/main/kotlin/platform/mixin/inspection/addedMembers/EnumConstantNameFormatInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/addedMembers/EnumConstantNameFormatInspection.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) +++ src/main/kotlin/platform/mixin/inspection/addedMembers/EnumConstantNameFormatInspection.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -0,0 +1,165 @@ +/* + * 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.inspection.addedMembers + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.util.RegexConverter +import com.demonwav.mcdev.util.doBindText +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.regexValidator +import com.demonwav.mcdev.util.toRegexOrDefault +import com.demonwav.mcdev.util.toRegexOrNull +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.icons.AllIcons +import com.intellij.openapi.module.Module +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.PsiModificationTracker +import com.intellij.ui.components.JBLabel +import com.intellij.ui.dsl.builder.COLUMNS_SHORT +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.xmlb.annotations.Attribute +import com.siyeh.ig.fixes.RenameFix +import java.util.regex.Pattern +import javax.swing.JComponent + +class EnumConstantNameFormatInspection : AbstractAddedMembersInspection() { + @JvmField + var validNameFormat = "^{MODID}_.+" + + @Attribute(converter = RegexConverter::class) + @JvmField + var validNameFixSearch = "^.+$".toRegex() + + @JvmField + var validNameFixReplace = "{MODID}_$0" + + override fun getStaticDescription() = "Reports enum constants that are not properly prefixed by the mod id" + + override fun visitAddedField(holder: ProblemsHolder, field: PsiField) { + if (field !is PsiEnumConstant) { + return + } + + val module = field.findModule() ?: return + + val validNameFormat = getValidNameFormat(module) ?: return + val name = field.name + if (validNameFormat.matches(name)) { + return + } + + // try to get a quick fix + val fixed = try { + validNameFixSearch.replace(name, validNameFixReplace) + .replace("{MODID}", convertModIdToScreamingSnakeCase(holder.project.name)) + } catch (_: RuntimeException) { + null + } + + if (fixed != null && StringUtil.isJavaIdentifier(fixed) && validNameFormat.matches(fixed)) { + holder.registerProblem( + field.nameIdentifier, + "Name does not match the pattern for added mixin members: \"${this.validNameFormat}\"", + RenameFix(fixed) + ) + } else { + holder.registerProblem( + field.nameIdentifier, + "Name does not match the pattern for added mixin members: \"${this.validNameFormat}\"", + ) + } + } + + override fun visitAddedMethod(holder: ProblemsHolder, method: PsiMethod, isInherited: Boolean) { + } + + private fun getValidNameFormat(module: Module): Regex? { + return CachedValuesManager.getManager(module.project).getCachedValue(module) { + CachedValueProvider.Result(getValidNameFormatUncached(module), PsiModificationTracker.MODIFICATION_COUNT) + } + } + + private fun getValidNameFormatUncached(module: Module): Regex? { + val modules = MinecraftFacet.getInstance(module)?.modules ?: emptyList() + val possibleModIds = setOf(module.project.name) + modules.flatMap { it.modIds } + val validPrefixes = possibleModIds.map { convertModIdToScreamingSnakeCase(it) } + + return this.validNameFormat.replace( + "{MODID}", + "(?:${validPrefixes.joinToString("|") { Pattern.quote(it) }})" + ).toRegexOrNull() + } + + private fun convertModIdToScreamingSnakeCase(modId: String): String { + return buildString(modId.length) { + for ((i, ch) in modId.withIndex()) { + if (!ch.isJavaIdentifierPart()) { + append('_') + } else if (i > 0 && ch.isUpperCase() && modId[i - 1].isLowerCase()) { + append('_') + append(ch) + } else { + if (i == 0 && !ch.isJavaIdentifierStart()) { + append('_') + } + append(ch.uppercase()) + } + } + }.ifEmpty { "_" } + } + + override fun createOptionsPanel(): JComponent { + return panel { + row { + val toolTip = "\"{MODID}\" is replaced with the mod ID, converted to SCREAMING_SNAKE_CASE." + label("Valid name format:") + .applyToComponent { horizontalTextPosition = JBLabel.LEFT } + .applyToComponent { icon = AllIcons.General.ContextHelp } + .applyToComponent { toolTipText = toolTip } + textField() + .doBindText(::validNameFormat) + .columns(COLUMNS_SHORT) + } + row("Valid name fix search:") { + textField() + .doBindText({ validNameFixSearch.pattern }, { validNameFixSearch = it.toRegexOrDefault("^.+$") }) + .columns(COLUMNS_SHORT) + .regexValidator() + } + row { + val toolTip = + "Uses regex replacement syntax after matching from the regex in the option above.
" + + "\"{MODID}\" is replaced with the mod ID, converted to SCREAMING_SNAKE_CASE." + label("Valid name fix replace:") + .applyToComponent { horizontalTextPosition = JBLabel.LEFT } + .applyToComponent { icon = AllIcons.General.ContextHelp } + .applyToComponent { toolTipText = toolTip } + textField().doBindText(::validNameFixReplace).columns(COLUMNS_SHORT) + } + } + } +} Index: src/main/kotlin/platform/neoforge/NeoForgeModule.kt =================================================================== --- src/main/kotlin/platform/neoforge/NeoForgeModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/neoforge/NeoForgeModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -26,6 +26,7 @@ import com.demonwav.mcdev.inspection.IsCancelled import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.forge.ForgeModule import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants import com.demonwav.mcdev.util.SourceType import com.demonwav.mcdev.util.nullable @@ -48,11 +49,15 @@ var mcmod by nullable { facet.findFile(NeoForgeConstants.MCMOD_INFO, SourceType.RESOURCE) } private set + var modsToml by nullable { facet.findFile(NeoForgeConstants.MODS_TOML_PATH, SourceType.RESOURCE) } + private set override val moduleType = NeoForgeModuleType override val type = PlatformType.NEOFORGE override val icon = PlatformAssets.NEOFORGE_ICON + override fun computeModIds() = ForgeModule.getModsFromModsToml(project, modsToml) + override val eventListenerGenSupport: EventListenerGenerationSupport = NeoForgeEventListenerGenerationSupport() override fun init() { Index: src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt =================================================================== --- src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -31,4 +31,5 @@ const val MCMOD_INFO = "mcmod.info" const val PACK_MCMETA = "pack.mcmeta" const val MODS_TOML = "neoforge.mods.toml" + const val MODS_TOML_PATH = "META-INF/$MODS_TOML" } Index: src/main/kotlin/platform/sponge/SpongeModule.kt =================================================================== --- src/main/kotlin/platform/sponge/SpongeModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/sponge/SpongeModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -27,6 +27,7 @@ import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType import com.demonwav.mcdev.platform.sponge.util.SpongeConstants +import com.demonwav.mcdev.platform.sponge.util.spongePluginClassId import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions @@ -36,6 +37,8 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.AnnotatedElementsSearch import org.jetbrains.uast.UClass import org.jetbrains.uast.UIdentifier import org.jetbrains.uast.toUElementOfType @@ -46,6 +49,20 @@ override val type = PlatformType.SPONGE override val icon = PlatformAssets.SPONGE_ICON + override fun computeModIds(): List { + return findPluginsWithAnnotation(SpongeConstants.PLUGIN_ANNOTATION) + findPluginsWithAnnotation(SpongeConstants.JVM_PLUGIN_ANNOTATION) + } + + private fun findPluginsWithAnnotation(annotation: String): List { + val pluginAnnotation = + JavaPsiFacade.getInstance(project).findClass(annotation, GlobalSearchScope.allScope(project)) + ?: return emptyList() + return AnnotatedElementsSearch.searchElements(pluginAnnotation, GlobalSearchScope.moduleScope(module), PsiClass::class.java) + .mapNotNull { pluginClass -> + pluginClass.spongePluginClassId() + } + } + override val eventListenerGenSupport: EventListenerGenerationSupport = SpongeEventListenerGenerationSupport() override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?) = Index: src/main/kotlin/platform/velocity/VelocityModule.kt =================================================================== --- src/main/kotlin/platform/velocity/VelocityModule.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/platform/velocity/VelocityModule.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -26,11 +26,16 @@ import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType import com.demonwav.mcdev.platform.velocity.util.VelocityConstants +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.findAnnotation import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.AnnotatedElementsSearch import org.jetbrains.uast.UClass import org.jetbrains.uast.UIdentifier import org.jetbrains.uast.toUElementOfType @@ -40,6 +45,16 @@ override val type = PlatformType.VELOCITY override val icon = PlatformAssets.VELOCITY_ICON + override fun computeModIds(): List { + val pluginAnnotation = + JavaPsiFacade.getInstance(project).findClass(VelocityConstants.PLUGIN_ANNOTATION, GlobalSearchScope.allScope(project)) + ?: return emptyList() + return AnnotatedElementsSearch.searchElements(pluginAnnotation, GlobalSearchScope.moduleScope(module), PsiClass::class.java) + .mapNotNull { pluginClass -> + pluginClass.findAnnotation(VelocityConstants.PLUGIN_ANNOTATION)?.findAttributeValue("id")?.constantStringValue + } + } + override val eventListenerGenSupport: EventListenerGenerationSupport = VelocityEventListenerGenerationSupport() override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?): Boolean = true Index: src/main/kotlin/util/swing-utils.kt =================================================================== --- src/main/kotlin/util/swing-utils.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/util/swing-utils.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -22,10 +22,22 @@ import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.observable.util.bindEnabled +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ComponentValidator +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBTextField import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.layout.ValidationInfoBuilder +import com.intellij.util.xmlb.Converter import java.awt.Component import java.awt.event.HierarchyEvent +import java.util.function.Supplier +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException import javax.swing.JComponent +import javax.swing.event.DocumentEvent +import kotlin.reflect.KMutableProperty0 fun Component.onShown(func: (HierarchyEvent) -> Unit) { addHierarchyListener { event -> @@ -49,3 +61,56 @@ } return this } + +fun Cell.doBindText(property: KMutableProperty0): Cell { + return doBindText(property.getter, property.setter) +} + +fun Cell.doBindText(getter: () -> String, setter: (String) -> Unit): Cell { + component.text = getter() + component.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + setter(component.text) + } + }) + return this +} + +fun Cell>.doBindItem(property: KMutableProperty0): Cell> { + component.selectedItem = property.get() + component.addActionListener { + @Suppress("UNCHECKED_CAST") + val selectedItem = component.selectedItem as T? + if (selectedItem != null) { + property.set(selectedItem) + } + } + return this +} + +fun Cell.regexValidator(): Cell { + var hasRegisteredValidator = false + component.onShown { + if (!hasRegisteredValidator) { + hasRegisteredValidator = true + val disposable = DialogWrapper.findInstance(component)?.disposable ?: return@onShown + ComponentValidator(disposable).withValidator( + Supplier { + try { + Pattern.compile(component.text) + null + } catch (_: PatternSyntaxException) { + ValidationInfoBuilder(component).error("Invalid regex") + } + } + ).andRegisterOnDocumentListener(component).installOn(component) + } + } + return this +} + +class RegexConverter : Converter() { + override fun toString(value: Regex) = value.pattern + + override fun fromString(value: String) = value.toRegexOrNull() +} Index: src/main/kotlin/util/utils.kt =================================================================== --- src/main/kotlin/util/utils.kt (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/kotlin/util/utils.kt (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -48,8 +48,10 @@ import java.lang.invoke.MethodHandles import java.util.Locale import java.util.concurrent.CancellationException +import java.util.regex.PatternSyntaxException import kotlin.math.min import kotlin.reflect.KClass +import org.intellij.lang.annotations.Language import org.jetbrains.annotations.NonNls import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.runAsync @@ -337,6 +339,18 @@ fun String.decapitalize(): String = replaceFirstChar { it.lowercase(Locale.ENGLISH) } +fun @receiver:Language("RegExp") String.toRegexOrNull(): Regex? { + return try { + this.toRegex() + } catch (_: PatternSyntaxException) { + null + } +} + +fun String.toRegexOrDefault(@Language("RegExp") default: String): Regex { + return this.toRegexOrNull() ?: default.toRegex() +} + // Bit of a hack, but this allows us to get the class object for top level declarations without having to // put the whole class name in as a string (easier to refactor, etc.) @Suppress("NOTHING_TO_INLINE") // In order for this to work this function must be `inline` Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 7a5d8596037b4026aafdbcb087083559f7127337) +++ src/main/resources/META-INF/plugin.xml (revision 9ab87fb8faf78967c1ebb9900951156f4a06ea58) @@ -1168,6 +1168,14 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.addedMembers.AddedMembersNameFormatInspection"/> +